Enhance petition form with ID card input validation and consent checkbox; improve error handling in Turnstile response

This commit is contained in:
fISHIE
2025-12-17 13:11:24 +05:00
parent 1e44b19a70
commit 1cfb0c55a8
3 changed files with 168 additions and 22 deletions

View File

@@ -56,15 +56,18 @@ defer></script>
<form id="signature-form">
<div class="form-group">
<label class="lang-en-content">Full Name</label>
<label class="lang-en-content">Full Name (As on ID card)</label>
<label class="dhivehi lang-dv-content">ފުރިހަމަ ނަން</label>
<input type="text" id="name" name="name" required>
<input type="text" id="name" name="name" minlength="3" maxlength="30" required>
</div>
<div class="form-group">
<div class="form-group idcard-group">
<label class="lang-en-content">ID Card Number</label>
<label class="dhivehi lang-dv-content">ކާޑު ނަންބަރު</label>
<input type="text" id="idCard" name="idCard" required>
<div class="idcard-input">
<span class="idcard-prefix">A</span>
<input type="tel" inputmode="numeric" pattern="\d{6}" id="idCard" name="idCard" maxlength="6" required placeholder="123456" aria-label="ID number, 6 digits" dir="ltr">
</div>
</div>
<div class="form-group">
@@ -75,7 +78,15 @@ defer></script>
</div>
</div>
<div id="form-message" class="form-message"></div>
<div class="form-group consent-group">
<label class="consent-label">
<input type="checkbox" id="confirm-consent" name="confirmConsent" required>
<span class="lang-en-content">I acknowledge my signature and information will be sent to the Parliament Petition Committee, and that the information I provide is true.</span>
<span class="dhivehi lang-dv-content">I acknowledge my signature and information will be sent to the Parliament Petition Committee, and that the information I provide is true.</span>
</label>
</div>
<div id="form-message" class="form-message"></div>
<!-- Cloudflare Turnstile: add callbacks so auto-render uses our handlers -->
<div class="cf-turnstile" data-sitekey="0x4AAAAAACHH4QC3wIhkCuhd"
@@ -122,7 +133,14 @@ defer></script>
// 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></script>
// 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></script>
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></script>
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></script>
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></script>
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></script>
// 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);
});
</script>

View File

@@ -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%;

View File

@@ -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<TurnstileResponse>(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<TurnstileResponse>(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
}