mirror of
https://github.com/MvDevsUnion/WPetition.git
synced 2026-07-17 21:10:15 +00:00
Enhance petition form with ID card input validation and consent checkbox; improve error handling in Turnstile response
This commit is contained in:
+84
-9
@@ -56,15 +56,18 @@ defer></script>
|
|||||||
|
|
||||||
<form id="signature-form">
|
<form id="signature-form">
|
||||||
<div class="form-group">
|
<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>
|
<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>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group idcard-group">
|
||||||
<label class="lang-en-content">ID Card Number</label>
|
<label class="lang-en-content">ID Card Number</label>
|
||||||
<label class="dhivehi lang-dv-content">ކާޑު ނަންބަރު</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>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -75,7 +78,15 @@ defer></script>
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Cloudflare Turnstile: add callbacks so auto-render uses our handlers -->
|
||||||
<div class="cf-turnstile" data-sitekey="0x4AAAAAACHH4QC3wIhkCuhd"
|
<div class="cf-turnstile" data-sitekey="0x4AAAAAACHH4QC3wIhkCuhd"
|
||||||
@@ -122,7 +133,14 @@ defer></script>
|
|||||||
// Extract petition ID from URL query parameter
|
// Extract petition ID from URL query parameter
|
||||||
function getPetitionIdFromUrl() {
|
function getPetitionIdFromUrl() {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
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
|
// Get language from URL query parameter
|
||||||
@@ -166,6 +184,10 @@ defer></script>
|
|||||||
|
|
||||||
// Update URL
|
// Update URL
|
||||||
updateUrlWithLang(lang);
|
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
|
// Fetch petition data
|
||||||
@@ -182,10 +204,32 @@ defer></script>
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
displayPetition(data);
|
displayPetition(data);
|
||||||
} catch (error) {
|
} 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
|
// Display petition data
|
||||||
function displayPetition(data) {
|
function displayPetition(data) {
|
||||||
// Store petition data for tweet generation
|
// Store petition data for tweet generation
|
||||||
@@ -275,7 +319,14 @@ defer></script>
|
|||||||
const canvas = document.getElementById('signature-pad');
|
const canvas = document.getElementById('signature-pad');
|
||||||
const ctx = canvas.getContext('2d');
|
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.strokeStyle = '#000';
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
@@ -398,7 +449,8 @@ defer></script>
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const name = document.getElementById('name').value;
|
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();
|
const signature = generateSVG();
|
||||||
|
|
||||||
if (!signature) {
|
if (!signature) {
|
||||||
@@ -406,6 +458,21 @@ defer></script>
|
|||||||
return;
|
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 petitionId = getPetitionIdFromUrl();
|
||||||
const apiUrl = `${API_CONFIG.baseUrl}/api/Sign/petition/${petitionId}`;
|
const apiUrl = `${API_CONFIG.baseUrl}/api/Sign/petition/${petitionId}`;
|
||||||
|
|
||||||
@@ -619,6 +686,14 @@ defer></script>
|
|||||||
// Set up tweet modal listeners
|
// Set up tweet modal listeners
|
||||||
setupTweetModalListeners();
|
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);
|
fetchPetition(petitionId);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+55
-11
@@ -223,11 +223,61 @@ body {
|
|||||||
border-color: #007bff;
|
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 {
|
.signature-pad-container {
|
||||||
border: 2px solid #ddd;
|
border: 2px solid #ddd;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
display: inline-block;
|
display: block;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
@@ -235,6 +285,10 @@ body {
|
|||||||
#signature-pad {
|
#signature-pad {
|
||||||
display: block;
|
display: block;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signature-actions {
|
.signature-actions {
|
||||||
@@ -313,16 +367,6 @@ body {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#signature-pad {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-pad-container {
|
|
||||||
width: 100%;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-buttons {
|
.form-buttons {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using MongoDB.Driver;
|
|||||||
using Submission.Api.Dto;
|
using Submission.Api.Dto;
|
||||||
using Submission.Api.Models;
|
using Submission.Api.Models;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Submission.Api.Controllers
|
namespace Submission.Api.Controllers
|
||||||
{
|
{
|
||||||
@@ -96,7 +97,12 @@ namespace Submission.Api.Controllers
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Invalid token - reject submission
|
// 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 response = await _httpClient.PostAsync(SiteverifyUrl, postContent);
|
||||||
var stringContent = await response.Content.ReadAsStringAsync();
|
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)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@@ -201,8 +211,25 @@ namespace Submission.Api.Controllers
|
|||||||
|
|
||||||
public class TurnstileResponse
|
public class TurnstileResponse
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("success")]
|
||||||
public bool Success { get; set; }
|
public bool Success { get; set; }
|
||||||
|
|
||||||
|
// Cloudflare returns "error-codes" (with a hyphen) — map it explicitly
|
||||||
|
[JsonPropertyName("error-codes")]
|
||||||
public string[] ErrorCodes { get; set; }
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user