From 1cfb0c55a8dfed99cabd75c01791c83265be56a3 Mon Sep 17 00:00:00 2001 From: fISHIE <83373559+WhoIsFishie@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:11:24 +0500 Subject: [PATCH] Enhance petition form with ID card input validation and consent checkbox; improve error handling in Turnstile response --- Frontend/index.html | 93 ++++++++++++++++++-- Frontend/style.css | 66 +++++++++++--- Submission.Api/Controllers/SignController.cs | 31 ++++++- 3 files changed, 168 insertions(+), 22 deletions(-) diff --git a/Frontend/index.html b/Frontend/index.html index 21e02ac..10dc094 100644 --- a/Frontend/index.html +++ b/Frontend/index.html @@ -56,15 +56,18 @@ defer>
- + - +
-
+
- +
+ A + +
@@ -75,7 +78,15 @@ defer>
-
+ + +
// Extract petition ID from URL query parameter function getPetitionIdFromUrl() { const urlParams = new URLSearchParams(window.location.search); - return urlParams.get('id'); + const id = urlParams.get('id'); + + // Hardcoded mapping for cleaner URLs + if (id === 'dataprotection') { + return '7a315446-3c63-40cc-b382-86152bd8e591'; + } + + return id; } // Get language from URL query parameter @@ -166,6 +184,10 @@ defer> // Update URL updateUrlWithLang(lang); + // Set document direction for RTL languages (Dhivehi uses RTL script) + try { + document.documentElement.dir = (lang === 'dv') ? 'rtl' : 'ltr'; + } catch (e) { } } // Fetch petition data @@ -182,10 +204,32 @@ defer> const data = await response.json(); displayPetition(data); } catch (error) { - showError(`Failed to load petition: ${error.message}`); + // If fetching fails, show a visible dev notice and load dummy data + console.warn('Failed to fetch petition, falling back to dummy data.', error); + const errorDiv = document.getElementById('error'); + errorDiv.textContent = 'Failed to load petition from server — showing dummy data for development.'; + errorDiv.style.display = 'block'; + displayPetition(getDummyPetition(petitionId)); } } + // Return a dummy petition object useful for local development + function getDummyPetition(petitionId) { + const now = new Date(); + return { + id: petitionId || 'dev-petition', + nameEng: 'Demo Petition: Improve Local Services', + nameDhiv: 'Demo Petition', + startDate: now.toLocaleDateString(), + signatureCount: 42, + authorDetails: { + name: 'Demo Author' + }, + petitionBodyEng: 'This is dummy petition content to enable local development. Replace with real data when the API is available.', + petitionBodyDhiv: 'Demo petition content (Dhivehi)' + }; + } + // Display petition data function displayPetition(data) { // Store petition data for tweet generation @@ -275,7 +319,14 @@ defer> const canvas = document.getElementById('signature-pad'); const ctx = canvas.getContext('2d'); - // Set up canvas style + // Resize canvas to match its display size (fixes coordinate offset bug) + const rect = canvas.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + canvas.width = rect.width; + canvas.height = rect.height; + } + + // Re-apply context settings after resize ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.lineCap = 'round'; @@ -398,7 +449,8 @@ defer> e.preventDefault(); const name = document.getElementById('name').value; - const idCard = document.getElementById('idCard').value; + const idCardDigits = document.getElementById('idCard').value; + const idCardPattern = /^\d{6}$/; const signature = generateSVG(); if (!signature) { @@ -406,6 +458,21 @@ defer> return; } + // Ensure the signer has confirmed consent/accuracy + const consentElem = document.getElementById('confirm-consent'); + const consentChecked = consentElem ? consentElem.checked : true; + if (!consentChecked) { + showFormMessage('Please confirm that your submission will be sent to the Parliament Petition Committee and that your information is true.', 'error'); + return; + } + + // Validate ID digits (6 digits) and assemble full ID with prefix 'A' + if (!idCardPattern.test(idCardDigits)) { + showFormMessage('Please enter your ID number as 6 digits (numbers only).', 'error'); + return; + } + const idCard = 'A' + idCardDigits; + const petitionId = getPetitionIdFromUrl(); const apiUrl = `${API_CONFIG.baseUrl}/api/Sign/petition/${petitionId}`; @@ -619,6 +686,14 @@ defer> // Set up tweet modal listeners setupTweetModalListeners(); + // Make clicking the 'A' prefix focus the ID input + document.querySelectorAll('.idcard-prefix').forEach(prefix => { + prefix.addEventListener('click', () => { + const input = prefix.parentElement.querySelector('input[id="idCard"]'); + if (input) input.focus(); + }); + }); + fetchPetition(petitionId); }); diff --git a/Frontend/style.css b/Frontend/style.css index e400742..d0375c7 100644 --- a/Frontend/style.css +++ b/Frontend/style.css @@ -223,11 +223,61 @@ body { border-color: #007bff; } +/* ID card input: prefix + numeric field */ +.idcard-input { + display: flex; + align-items: center; + gap: 0; + max-width: 260px; + border: 1px solid #ddd; + border-radius: 6px; + overflow: hidden; + background: #fff; + transition: box-shadow 0.15s ease, border-color 0.15s ease; +} + +.idcard-prefix { + padding: 10px 12px; + font-weight: 700; + display: inline-flex; + align-items: center; + justify-content: center; + color: #333; + background: transparent; + border: none; /* prefix no longer has its own border */ + cursor: text; +} + +.idcard-input input[type="tel"] { + padding: 10px; + width: 120px; + font-size: 16px; + text-align: left; + border: none; /* input no longer has its own border */ + outline: none; + background: transparent; +} + +/* RTL adjustments: reverse order but keep the wrapper border intact */ +:dir(rtl) .idcard-input { + flex-direction: row-reverse; +} + +:dir(rtl) .idcard-input input[type="tel"] { + text-align: left; /* keep digits LTR within RTL page */ +} + +/* Highlight wrapper on focus */ +.idcard-input:focus-within { + border-color: #007bff; + box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.08); +} + .signature-pad-container { border: 2px solid #ddd; border-radius: 6px; background-color: white; - display: inline-block; + display: block; cursor: crosshair; margin-bottom: 10px; } @@ -235,6 +285,10 @@ body { #signature-pad { display: block; touch-action: none; + width: 100%; + max-width: 100%; + height: auto; + aspect-ratio: 3; } .signature-actions { @@ -313,16 +367,6 @@ body { gap: 10px; } - #signature-pad { - width: 100%; - max-width: 100%; - } - - .signature-pad-container { - width: 100%; - overflow-x: auto; - } - .form-buttons { flex-direction: column; width: 100%; diff --git a/Submission.Api/Controllers/SignController.cs b/Submission.Api/Controllers/SignController.cs index 7ed7dbd..51af945 100644 --- a/Submission.Api/Controllers/SignController.cs +++ b/Submission.Api/Controllers/SignController.cs @@ -7,6 +7,7 @@ using MongoDB.Driver; using Submission.Api.Dto; using Submission.Api.Models; using System.Text.Json; +using System.Text.Json.Serialization; namespace Submission.Api.Controllers { @@ -96,7 +97,12 @@ namespace Submission.Api.Controllers else { // Invalid token - reject submission - return BadRequest($"Verification failed: {string.Join(", ", validation.ErrorCodes)}"); + // Make joining error codes null-safe to avoid ArgumentNullException + var errorCodes = validation?.ErrorCodes; + var errors = (errorCodes != null && errorCodes.Length > 0) + ? string.Join(", ", errorCodes) + : "unknown"; + return BadRequest($"Verification failed: {errors}"); } } @@ -186,7 +192,11 @@ namespace Submission.Api.Controllers var response = await _httpClient.PostAsync(SiteverifyUrl, postContent); var stringContent = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(stringContent); + Console.WriteLine("Turnstile response: " + stringContent); + + // deserialize with case-insensitive option; mapping for "error-codes" is handled by attribute on ErrorCodes + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + return JsonSerializer.Deserialize(stringContent, options); } catch (Exception) { @@ -201,8 +211,25 @@ namespace Submission.Api.Controllers public class TurnstileResponse { + [JsonPropertyName("success")] public bool Success { get; set; } + + // Cloudflare returns "error-codes" (with a hyphen) — map it explicitly + [JsonPropertyName("error-codes")] public string[] ErrorCodes { get; set; } + + [JsonPropertyName("challenge_ts")] + public string ChallengeTs { get; set; } + + public string Hostname { get; set; } + + public string Action { get; set; } + + // "cdata" may be present + public string Cdata { get; set; } + + // metadata is optional and can be an object + public JsonElement? Metadata { get; set; } } #endregion }