Files
WPetition/Frontend/index.html

702 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Petition Details</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
<link rel="preconnect" href="https://challenges.cloudflare.com">
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer></script>
</head>
<body>
<div class="container">
<div id="loading" class="loading">Loading petition...</div>
<div id="error" class="error" style="display: none;"></div>
<div id="petition-content" style="display: none;">
<div class="lang-switcher">
<button id="lang-en" class="lang-btn active">English</button>
<button id="lang-dv" class="lang-btn">ދިވެހި</button>
</div>
<div class="petition-header">
<h1 id="petition-name-eng" class="lang-en-content"></h1>
<h2 id="petition-name-dhiv" class="dhivehi lang-dv-content"></h2>
<div class="metadata">
<span class="start-date">Start Date: <span id="start-date"></span></span>
<span class="signature-count">Signatures: <span id="signature-count"></span></span>
</div>
</div>
<div class="author-details">
<h3 class="lang-en-content">Author Details</h3>
<h3 class="dhivehi lang-dv-content">ލިޔުންތެރިގެ މައުލޫމާތު</h3>
<p><strong class="lang-en-content">Name:</strong><strong class="dhivehi lang-dv-content">ނަން:</strong> <span id="author-name"></span></p>
<!-- <p><strong class="lang-en-content">NID:</strong><strong class="dhivehi lang-dv-content">ކާޑު ނަންބަރު:</strong> <span id="author-nid"></span></p> -->
</div>
<div class="petition-body">
<div class="lang-en-content">
<div id="petition-body-eng" class="body-content"></div>
</div>
<div class="lang-dv-content">
<div id="petition-body-dhiv" class="body-content dhivehi"></div>
</div>
</div>
<div class="signature-section">
<h3 class="lang-en-content">Sign this Petition</h3>
<h3 class="dhivehi lang-dv-content">މި މައްސަލައިގައި ސޮއި ކުރައްވާ</h3>
<form id="signature-form">
<div class="form-group">
<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" minlength="3" maxlength="30" required>
</div>
<div class="form-group idcard-group">
<label class="lang-en-content">ID Card Number</label>
<label class="dhivehi lang-dv-content">ކާޑު ނަންބަރު</label>
<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">
<label class="lang-en-content">Signature</label>
<label class="dhivehi lang-dv-content">ސޮއި</label>
<div class="signature-pad-container">
<canvas id="signature-pad" width="600" height="200"></canvas>
</div>
</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"
data-callback="onTurnstileSuccess"
data-error-callback="onTurnstileError"
data-expired-callback="onTurnstileExpired"></div>
<div class="form-buttons">
<button type="button" id="clear-signature" class="btn-secondary" title="Clear signature" aria-label="Clear signature">
<i class="fas fa-eraser"></i> Clear
</button>
<button type="submit" class="btn-primary" title="Submit signature" aria-label="Submit signature">
<i class="fas fa-paper-plane"></i> Submit
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Tweet Prompt Modal -->
<div id="tweet-modal" class="modal">
<div class="modal-content">
<span class="close-modal">&times;</span>
<h2 id="modal-title">Share this petition!</h2>
<p id="modal-text">Help spread the word by tweeting about this petition.</p>
<div class="modal-buttons">
<button id="tweet-button" class="btn-primary">
<i class="fab fa-twitter"></i> <span id="tweet-btn-text">Tweet Now</span>
</button>
<button id="skip-tweet" class="btn-secondary"><span id="skip-btn-text">Maybe Later</span></button>
</div>
</div>
</div>
<script>
// API Configuration - Change this for different environments
const API_CONFIG = {
baseUrl: '' // Empty for same-origin requests (Nginx proxies /api/* to backend)
};
let currentLang = 'en';
// Extract petition ID from URL query parameter
function getPetitionIdFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
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
function getLangFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
const lang = urlParams.get('lang');
return lang === 'dv' ? 'dv' : 'en';
}
// Update URL with current language
function updateUrlWithLang(lang) {
const urlParams = new URLSearchParams(window.location.search);
if (lang === 'en') {
urlParams.delete('lang');
} else {
urlParams.set('lang', lang);
}
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
window.history.pushState({}, '', newUrl);
}
// Switch language
function switchLanguage(lang) {
currentLang = lang;
// Update button states
document.getElementById('lang-en').classList.toggle('active', lang === 'en');
document.getElementById('lang-dv').classList.toggle('active', lang === 'dv');
// Show/hide content based on language
const enContent = document.querySelectorAll('.lang-en-content');
const dvContent = document.querySelectorAll('.lang-dv-content');
enContent.forEach(el => {
el.style.display = lang === 'en' ? '' : 'none';
});
dvContent.forEach(el => {
el.style.display = lang === 'dv' ? '' : 'none';
});
// 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
async function fetchPetition(petitionId) {
const apiUrl = `${API_CONFIG.baseUrl}/api/Sign/petition/${petitionId}`;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
displayPetition(data);
} catch (error) {
// 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
currentPetitionData = data;
document.getElementById('loading').style.display = 'none';
document.getElementById('petition-content').style.display = 'block';
document.getElementById('petition-name-eng').textContent = data.nameEng;
document.getElementById('petition-name-dhiv').textContent = data.nameDhiv;
document.getElementById('start-date').textContent = data.startDate;
document.getElementById('signature-count').textContent = data.signatureCount;
document.getElementById('author-name').textContent = data.authorDetails.name;
//document.getElementById('author-nid').textContent = data.authorDetails.nid;
// Convert markdown-style formatting to HTML (basic support)
document.getElementById('petition-body-eng').innerHTML = formatText(data.petitionBodyEng);
document.getElementById('petition-body-dhiv').innerHTML = formatText(data.petitionBodyDhiv);
// Set initial language display
switchLanguage(currentLang);
}
// Parse Markdown to HTML
function formatText(text) {
return marked.parse(text);
}
// Show error message
function showError(message) {
document.getElementById('loading').style.display = 'none';
const errorDiv = document.getElementById('error');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
// Signature pad functionality
let isDrawing = false;
let signaturePaths = [];
let currentPath = [];
// Turnstile widget state
let turnstileWidgetId = null;
let turnstileToken = null;
function onTurnstileSuccess(token) {
turnstileToken = token;
}
function onTurnstileError() {
turnstileToken = null;
}
function onTurnstileExpired() {
turnstileToken = null;
}
// Try to render Turnstile widget when API is available
function ensureTurnstileRendered() {
const container = document.querySelector('.cf-turnstile');
if (!container) return;
// If Turnstile already auto-rendered an iframe inside the container, don't render again
if (container.children.length > 0) {
// mark as auto-rendered
turnstileWidgetId = -1;
return;
}
if (window.turnstile && turnstileWidgetId === null) {
try {
turnstileWidgetId = turnstile.render(container, {
sitekey: container.getAttribute('data-sitekey'),
callback: onTurnstileSuccess,
'error-callback': onTurnstileError,
'expired-callback': onTurnstileExpired
});
} catch (e) {
// If render fails, leave the element as-is (script may auto-render)
}
} else if (!window.turnstile) {
// Retry until turnstile script is available
setTimeout(ensureTurnstileRendered, 200);
}
}
function initSignaturePad() {
const canvas = document.getElementById('signature-pad');
const ctx = canvas.getContext('2d');
// 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';
ctx.lineJoin = 'round';
// Mouse events
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
// Touch events
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', stopDrawing);
// Clear button
document.getElementById('clear-signature').addEventListener('click', clearSignature);
}
function getMousePos(canvas, evt) {
const rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
function getTouchPos(canvas, evt) {
const rect = canvas.getBoundingClientRect();
return {
x: evt.touches[0].clientX - rect.left,
y: evt.touches[0].clientY - rect.top
};
}
function startDrawing(e) {
isDrawing = true;
const canvas = document.getElementById('signature-pad');
const pos = getMousePos(canvas, e);
currentPath = [pos];
}
function draw(e) {
if (!isDrawing) return;
const canvas = document.getElementById('signature-pad');
const ctx = canvas.getContext('2d');
const pos = getMousePos(canvas, e);
currentPath.push(pos);
ctx.beginPath();
ctx.moveTo(currentPath[currentPath.length - 2].x, currentPath[currentPath.length - 2].y);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
}
function stopDrawing() {
if (isDrawing && currentPath.length > 0) {
signaturePaths.push([...currentPath]);
currentPath = [];
}
isDrawing = false;
}
function handleTouchStart(e) {
e.preventDefault();
isDrawing = true;
const canvas = document.getElementById('signature-pad');
const pos = getTouchPos(canvas, e);
currentPath = [pos];
}
function handleTouchMove(e) {
e.preventDefault();
if (!isDrawing) return;
const canvas = document.getElementById('signature-pad');
const ctx = canvas.getContext('2d');
const pos = getTouchPos(canvas, e);
currentPath.push(pos);
ctx.beginPath();
ctx.moveTo(currentPath[currentPath.length - 2].x, currentPath[currentPath.length - 2].y);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
}
function clearSignature() {
const canvas = document.getElementById('signature-pad');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
signaturePaths = [];
currentPath = [];
}
function generateSVG() {
if (signaturePaths.length === 0) {
return null;
}
let pathData = '';
signaturePaths.forEach(path => {
if (path.length > 0) {
pathData += `M ${path[0].x} ${path[0].y} `;
for (let i = 1; i < path.length; i++) {
pathData += `L ${path[i].x} ${path[i].y} `;
}
}
});
const svg = `<svg width="600" height="200" xmlns="http://www.w3.org/2000/svg"><path d="${pathData}" stroke="black" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
return svg;
}
// Form submission
async function submitSignature(e) {
e.preventDefault();
const name = document.getElementById('name').value;
const idCardDigits = document.getElementById('idCard').value;
const idCardPattern = /^\d{6}$/;
const signature = generateSVG();
if (!signature) {
showFormMessage('Please provide your signature.', 'error');
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}`;
// Ensure Turnstile widget is rendered
ensureTurnstileRendered();
// Obtain token: prefer the callback value, fall back to getResponse or execute
let token = turnstileToken;
if ((!token || token.length === 0) && window.turnstile && turnstileWidgetId !== null) {
try {
if (typeof turnstile.getResponse === 'function') {
let resp = null;
try {
if (turnstileWidgetId !== null && turnstileWidgetId >= 0) {
resp = turnstile.getResponse(turnstileWidgetId);
} else {
// try without id (auto-rendered widgets may be retrievable this way)
resp = turnstile.getResponse();
}
} catch (e) {
resp = null;
}
if (resp) token = resp;
}
if ((!token || token.length === 0) && typeof turnstile.execute === 'function') {
// Trigger challenge; wait up to ~5s for callback to set token
try { turnstile.execute(turnstileWidgetId); } catch (err) { }
token = await new Promise((resolve) => {
let attempts = 0;
const interval = setInterval(() => {
attempts++;
if (turnstileToken) {
clearInterval(interval);
resolve(turnstileToken);
} else if (attempts > 25) { // ~5 seconds
clearInterval(interval);
resolve(null);
}
}, 200);
});
}
} catch (err) {
token = null;
}
}
if (!token) {
showFormMessage('Please complete the captcha challenge.', 'error');
return;
}
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'accept': '*/*',
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
idCard: idCard,
signature: signature,
turnstileToken: token
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
showFormMessage('Signature submitted successfully!', 'success');
// Show tweet prompt modal
showTweetModal();
document.getElementById('signature-form').reset();
clearSignature();
// Refresh petition data to update signature count
setTimeout(() => {
fetchPetition(petitionId);
}, 1000);
} catch (error) {
showFormMessage(`Failed to submit signature: ${error.message}`, 'error');
} finally {
// Reset the Turnstile widget so a fresh token is required next time
try {
if (window.turnstile && turnstileWidgetId !== null) {
turnstile.reset(turnstileWidgetId);
}
} catch (e) { }
turnstileToken = null;
}
}
function showFormMessage(message, type) {
const messageDiv = document.getElementById('form-message');
messageDiv.textContent = message;
messageDiv.className = `form-message ${type}`;
messageDiv.style.display = 'block';
setTimeout(() => {
messageDiv.style.display = 'none';
}, 5000);
}
// Tweet Modal Functions
let currentPetitionData = null;
function updateModalLanguage() {
const translations = {
en: {
title: 'Share this petition!',
text: 'Help spread the word by tweeting about this petition.',
tweetBtn: 'Tweet Now',
skipBtn: 'Maybe Later'
},
dv: {
title: 'Share this petition!',
text: 'Help spread the word by tweeting about this petition.',
tweetBtn: 'Tweet Now',
skipBtn: 'Maybe Later'
}
};
const t = translations[currentLang];
document.getElementById('modal-title').textContent = t.title;
document.getElementById('modal-text').textContent = t.text;
document.getElementById('tweet-btn-text').textContent = t.tweetBtn;
document.getElementById('skip-btn-text').textContent = t.skipBtn;
}
function showTweetModal() {
updateModalLanguage();
const modal = document.getElementById('tweet-modal');
modal.classList.add('show');
}
function hideTweetModal() {
const modal = document.getElementById('tweet-modal');
modal.classList.remove('show');
}
function generateTweetText() {
if (!currentPetitionData) return '';
const petitionName = currentLang === 'dv' ? currentPetitionData.nameDhiv : currentPetitionData.nameEng;
const petitionUrl = `${window.location.origin}${window.location.pathname}?id=${currentPetitionData.id}`;
return `I just signed "${petitionName}"! \n\nAdd your signature: ${petitionUrl}`;
}
function openTwitterIntent() {
const tweetText = generateTweetText();
const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}`;
console.log(twitterUrl);
window.open(twitterUrl, '_blank');
hideTweetModal();
}
function setupTweetModalListeners() {
const modal = document.getElementById('tweet-modal');
const tweetButton = document.getElementById('tweet-button');
const skipButton = document.getElementById('skip-tweet');
const closeButton = document.querySelector('.close-modal');
// Tweet button click
tweetButton.addEventListener('click', openTwitterIntent);
// Skip button click
skipButton.addEventListener('click', hideTweetModal);
// Close X click
closeButton.addEventListener('click', hideTweetModal);
// Click outside modal
modal.addEventListener('click', (e) => {
if (e.target === modal) {
hideTweetModal();
}
});
}
// Initialize on page load
window.addEventListener('DOMContentLoaded', () => {
const petitionId = getPetitionIdFromUrl();
if (!petitionId) {
showError('No petition ID found in URL. Please provide a valid petition URL.');
return;
}
// Get initial language from URL
currentLang = getLangFromUrl();
// Set up language switcher event listeners
document.getElementById('lang-en').addEventListener('click', () => switchLanguage('en'));
document.getElementById('lang-dv').addEventListener('click', () => switchLanguage('dv'));
// Initialize signature pad
initSignaturePad();
// Render Turnstile (retry until script available)
ensureTurnstileRendered();
// Set up form submission
document.getElementById('signature-form').addEventListener('submit', submitSignature);
// 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>
</body>
</html>