mirror of
https://github.com/MvDevsUnion/WPetition.git
synced 2026-07-15 12:00:08 +00:00
create petitions
added support to create petitions from the website
This commit is contained in:
@@ -5,7 +5,8 @@
|
|||||||
"Bash(docker compose:*)",
|
"Bash(docker compose:*)",
|
||||||
"Bash(chmod:*)",
|
"Bash(chmod:*)",
|
||||||
"Bash(tree:*)",
|
"Bash(tree:*)",
|
||||||
"Bash(./ci-helper:*)"
|
"Bash(./ci-helper:*)",
|
||||||
|
"Bash(npm run build:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
# WPetition
|
|
||||||
|
|
||||||
a very basic and simple frontend to show how easy it is to setup the ui for this
|
|
||||||
|
|
||||||
```
|
|
||||||
baseUrl: 'http://localhost:5299'
|
|
||||||
```
|
|
||||||
make sure to edit this for prod pls or it wont work
|
|
||||||
Binary file not shown.
@@ -1,701 +0,0 @@
|
|||||||
<!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">×</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>
|
|
||||||
@@ -1,496 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Utheem';
|
|
||||||
src: url('fonts/utheem.woff') format('woff'),
|
|
||||||
url('fonts/utheem.ttf') format('truetype');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Shangu';
|
|
||||||
src: url('fonts/shangu.woff') format('woff'),
|
|
||||||
url('fonts/shangu.ttf') format('truetype');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.6;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-switcher {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-btn {
|
|
||||||
padding: 8px 20px;
|
|
||||||
border: 2px solid #007bff;
|
|
||||||
background-color: white;
|
|
||||||
color: #007bff;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-btn:hover {
|
|
||||||
background-color: #f0f8ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-btn.active {
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-btn:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #666;
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background-color: #fee;
|
|
||||||
border: 1px solid #fcc;
|
|
||||||
color: #c33;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.petition-header {
|
|
||||||
border-bottom: 2px solid #007bff;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.petition-header h1 {
|
|
||||||
font-size: 32px;
|
|
||||||
color: #222;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.petition-header h2 {
|
|
||||||
font-size: 24px;
|
|
||||||
color: #555;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.petition-header h2.dhivehi {
|
|
||||||
font-family: 'Shangu', 'Faruma', 'MV Faseyha', 'Waheed', 'Noto Sans Thaana', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dhivehi {
|
|
||||||
font-family: 'Utheem', 'Faruma', 'MV Faseyha', 'Waheed', 'Noto Sans Thaana', sans-serif;
|
|
||||||
direction: rtl;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 15px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata span {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata span span {
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-details {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-details h3 {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #007bff;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-details p {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.petition-body {
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.petition-body h3 {
|
|
||||||
font-size: 20px;
|
|
||||||
color: #007bff;
|
|
||||||
margin-top: 30px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-content {
|
|
||||||
padding: 15px;
|
|
||||||
background-color: #fafafa;
|
|
||||||
border-radius: 4px;
|
|
||||||
line-height: 1.8;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-content strong {
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-count {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-count span {
|
|
||||||
color: #007bff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-section {
|
|
||||||
margin-top: 40px;
|
|
||||||
padding-top: 30px;
|
|
||||||
border-top: 2px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-section h3 {
|
|
||||||
font-size: 24px;
|
|
||||||
color: #007bff;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input[type="text"] {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
border: 2px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 16px;
|
|
||||||
transition: border-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input[type="text"]:focus {
|
|
||||||
outline: none;
|
|
||||||
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: block;
|
|
||||||
cursor: crosshair;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#signature-pad {
|
|
||||||
display: block;
|
|
||||||
touch-action: none;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
aspect-ratio: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-actions {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary,
|
|
||||||
.btn-secondary {
|
|
||||||
padding: 12px 24px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background-color: #6c757d;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background-color: #545b62;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-message {
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-message.success {
|
|
||||||
background-color: #d4edda;
|
|
||||||
border: 1px solid #c3e6cb;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-message.error {
|
|
||||||
background-color: #f8d7da;
|
|
||||||
border: 1px solid #f5c6cb;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.container {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.petition-header h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.petition-header h2 {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-buttons {
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-buttons button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tweet Modal Styles */
|
|
||||||
.modal {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1000;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
animation: fadeIn 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.show {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background-color: white;
|
|
||||||
padding: 40px;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
||||||
max-width: 500px;
|
|
||||||
width: 90%;
|
|
||||||
position: relative;
|
|
||||||
animation: slideIn 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-modal {
|
|
||||||
position: absolute;
|
|
||||||
top: 15px;
|
|
||||||
right: 20px;
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #999;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-modal:hover {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content h2 {
|
|
||||||
font-size: 24px;
|
|
||||||
color: #222;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content p {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-buttons button {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-buttons .btn-primary {
|
|
||||||
background-color: #1DA1F2;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-buttons .btn-primary:hover {
|
|
||||||
background-color: #1a91da;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-buttons .btn-primary i {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
transform: translateY(-50px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.modal-content {
|
|
||||||
padding: 30px 20px;
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content h2 {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
+54
@@ -20,6 +20,7 @@
|
|||||||
"marked": "^17.0.1",
|
"marked": "^17.0.1",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.13.0",
|
||||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0"
|
||||||
},
|
},
|
||||||
@@ -2804,6 +2805,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -4163,6 +4176,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz",
|
||||||
|
"integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz",
|
||||||
|
"integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.13.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-signature-canvas": {
|
"node_modules/react-signature-canvas": {
|
||||||
"version": "1.1.0-alpha.2",
|
"version": "1.1.0-alpha.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-signature-canvas/-/react-signature-canvas-1.1.0-alpha.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-signature-canvas/-/react-signature-canvas-1.1.0-alpha.2.tgz",
|
||||||
@@ -4286,6 +4335,11 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"marked": "^17.0.1",
|
"marked": "^17.0.1",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.13.0",
|
||||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+11
-121
@@ -1,127 +1,17 @@
|
|||||||
import { useState, useRef } from "react";
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
import { usePetition } from "@/hooks/usePetition";
|
import { HomePage } from "@/pages/HomePage";
|
||||||
import { useLanguage } from "@/hooks/useLanguage";
|
import { PetitionPage } from "@/pages/PetitionPage";
|
||||||
import { submitSignature } from "@/lib/api";
|
import { CreatePetitionPage } from "@/pages/CreatePetitionPage";
|
||||||
import { LanguageSwitcher } from "@/components/layout/LanguageSwitcher";
|
|
||||||
import { LoadingState } from "@/components/layout/LoadingState";
|
|
||||||
import { ErrorState } from "@/components/layout/ErrorState";
|
|
||||||
import { PetitionHeader } from "@/components/petition/PetitionHeader";
|
|
||||||
import { AuthorCard } from "@/components/petition/AuthorCard";
|
|
||||||
import { PetitionBody } from "@/components/petition/PetitionBody";
|
|
||||||
import { SignatureForm } from "@/components/signature/SignatureForm";
|
|
||||||
import { TweetModal } from "@/components/TweetModal";
|
|
||||||
import { PenLine } from "lucide-react";
|
|
||||||
|
|
||||||
function getPetitionIdFromUrl(): string | null {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const id = urlParams.get("id");
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const petitionId = getPetitionIdFromUrl();
|
|
||||||
const { petition, loading, error, refetch } = usePetition(petitionId);
|
|
||||||
const { language, setLanguage } = useLanguage();
|
|
||||||
const [showTweetModal, setShowTweetModal] = useState(false);
|
|
||||||
const signatureFormRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const scrollToSignForm = () => {
|
|
||||||
signatureFormRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (data: {
|
|
||||||
name: string;
|
|
||||||
idCard: string;
|
|
||||||
signature: string;
|
|
||||||
turnstileToken: string;
|
|
||||||
}) => {
|
|
||||||
if (!petitionId) throw new Error("No petition ID");
|
|
||||||
|
|
||||||
await submitSignature(petitionId, data);
|
|
||||||
|
|
||||||
// Show tweet modal after successful submission
|
|
||||||
setShowTweetModal(true);
|
|
||||||
|
|
||||||
// Refresh petition data to update signature count
|
|
||||||
setTimeout(() => {
|
|
||||||
refetch();
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// No petition ID in URL
|
|
||||||
if (!petitionId) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-background p-5">
|
|
||||||
<div className="max-w-4xl mx-auto bg-card rounded-lg shadow-lg p-10">
|
|
||||||
<ErrorState message="No petition ID found in URL. Please provide a valid petition URL." />
|
|
||||||
</div>
|
|
||||||
<footer className="text-center text-slate-500 text-sm mt-6 pb-4">
|
|
||||||
Powered by Mv Devs Union
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-50/50 p-4 md:p-8 font-sans">
|
<BrowserRouter>
|
||||||
<div className="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-slate-100 p-6 md:p-10 animate-in fade-in duration-500 slide-in-from-bottom-4">
|
<Routes>
|
||||||
{loading ? (
|
<Route path="/" element={<HomePage />} />
|
||||||
<div className="min-h-[400px] flex flex-col justify-center">
|
<Route path="/Petition/:slug" element={<PetitionPage />} />
|
||||||
<LoadingState language={language} />
|
<Route path="/CreatePetition" element={<CreatePetitionPage />} />
|
||||||
</div>
|
</Routes>
|
||||||
) : (
|
</BrowserRouter>
|
||||||
<>
|
|
||||||
{error && <ErrorState message={error} />}
|
|
||||||
|
|
||||||
{petition && (
|
|
||||||
<div className="space-y-8">
|
|
||||||
<LanguageSwitcher
|
|
||||||
language={language}
|
|
||||||
onLanguageChange={setLanguage}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PetitionHeader petition={petition} language={language} />
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={scrollToSignForm}
|
|
||||||
className={`w-full flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors ${language === "dv" ? "flex-row-reverse dhivehi" : ""}`}
|
|
||||||
>
|
|
||||||
<PenLine className="w-5 h-5" />
|
|
||||||
{language === "dv" ? "މިހާރު ސޮއި ކުރައްވާ" : "Sign Now"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AuthorCard
|
|
||||||
author={petition.authorDetails}
|
|
||||||
language={language}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PetitionBody
|
|
||||||
bodyEng={petition.petitionBodyEng}
|
|
||||||
bodyDhiv={petition.petitionBodyDhiv}
|
|
||||||
language={language}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div ref={signatureFormRef}>
|
|
||||||
<SignatureForm language={language} onSubmit={handleSubmit} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TweetModal
|
|
||||||
open={showTweetModal}
|
|
||||||
onClose={() => setShowTweetModal(false)}
|
|
||||||
petition={petition}
|
|
||||||
language={language}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer className="text-center text-slate-500 text-sm mt-6 pb-4">
|
|
||||||
Powered by Mv Devs Union
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import type { PetitionDetails } from "@/types/petition";
|
import type { PetitionDetails } from "@/types/petition";
|
||||||
import { fetchPetition, getDummyPetition } from "@/lib/api";
|
import { fetchPetitionBySlug, getDummyPetition } from "@/lib/api";
|
||||||
|
|
||||||
interface UsePetitionResult {
|
interface UsePetitionResult {
|
||||||
petition: PetitionDetails | null;
|
petition: PetitionDetails | null;
|
||||||
@@ -9,15 +9,15 @@ interface UsePetitionResult {
|
|||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePetition(petitionId: string | null): UsePetitionResult {
|
export function usePetition(slug: string | null): UsePetitionResult {
|
||||||
const [petition, setPetition] = useState<PetitionDetails | null>(null);
|
const [petition, setPetition] = useState<PetitionDetails | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadPetition = useCallback(async () => {
|
const loadPetition = useCallback(async () => {
|
||||||
if (!petitionId) {
|
if (!slug) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError("No petition ID provided");
|
setError("No petition slug provided");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export function usePetition(petitionId: string | null): UsePetitionResult {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchPetition(petitionId);
|
const data = await fetchPetitionBySlug(slug);
|
||||||
setPetition(data);
|
setPetition(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -36,11 +36,11 @@ export function usePetition(petitionId: string | null): UsePetitionResult {
|
|||||||
setError(
|
setError(
|
||||||
"Failed to load petition from server — showing dummy data for development.",
|
"Failed to load petition from server — showing dummy data for development.",
|
||||||
);
|
);
|
||||||
setPetition(getDummyPetition(petitionId));
|
setPetition(getDummyPetition(slug));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [petitionId]);
|
}, [slug]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadPetition();
|
loadPetition();
|
||||||
|
|||||||
@@ -17,6 +17,69 @@ export async function fetchPetition(
|
|||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchPetitionBySlug(
|
||||||
|
slug: string,
|
||||||
|
): Promise<PetitionDetails> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/api/Sign/petition/by-slug/${slug}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PetitionFormData {
|
||||||
|
slug: string;
|
||||||
|
nameDhiv: string;
|
||||||
|
nameEng: string;
|
||||||
|
startDate: string; // dd-MM-yyyy
|
||||||
|
authorName: string;
|
||||||
|
authorNid: string;
|
||||||
|
petitionBodyDhiv: string;
|
||||||
|
petitionBodyEng: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmitPetitionResponse {
|
||||||
|
message: string;
|
||||||
|
petitionId: string;
|
||||||
|
slug: string;
|
||||||
|
fileName: string;
|
||||||
|
filePath: string;
|
||||||
|
authorId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitPetition(
|
||||||
|
data: PetitionFormData,
|
||||||
|
): Promise<SubmitPetitionResponse> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("Slug", data.slug);
|
||||||
|
formData.append("NameDhiv", data.nameDhiv);
|
||||||
|
formData.append("NameEng", data.nameEng);
|
||||||
|
formData.append("StartDate", data.startDate);
|
||||||
|
formData.append("AuthorName", data.authorName);
|
||||||
|
formData.append("AuthorNid", data.authorNid);
|
||||||
|
formData.append("PetitionBodyDhiv", data.petitionBodyDhiv);
|
||||||
|
formData.append("PetitionBodyEng", data.petitionBodyEng);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/api/Debug/upload-petition-form`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
export async function submitSignature(
|
export async function submitSignature(
|
||||||
petitionId: string,
|
petitionId: string,
|
||||||
submission: SignatureSubmission,
|
submission: SignatureSubmission,
|
||||||
@@ -39,9 +102,10 @@ export async function submitSignature(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dummy petition for development when API is not available
|
// Dummy petition for development when API is not available
|
||||||
export function getDummyPetition(petitionId: string): PetitionDetails {
|
export function getDummyPetition(slug: string): PetitionDetails {
|
||||||
return {
|
return {
|
||||||
id: petitionId || "dev-petition",
|
id: "dev-petition-id",
|
||||||
|
slug: slug || "demo-petition",
|
||||||
nameEng: "Demo Petition: Improve Local Services",
|
nameEng: "Demo Petition: Improve Local Services",
|
||||||
nameDhiv: "Demo Petition",
|
nameDhiv: "Demo Petition",
|
||||||
startDate: new Date().toLocaleDateString(),
|
startDate: new Date().toLocaleDateString(),
|
||||||
|
|||||||
@@ -0,0 +1,400 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { submitPetition, type PetitionFormData } from "@/lib/api";
|
||||||
|
import {
|
||||||
|
FileText,
|
||||||
|
Send,
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle,
|
||||||
|
AlertTriangle,
|
||||||
|
ExternalLink,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
function GuidelinesModal({ onAccept }: { onAccept: () => void }) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||||
|
<div className="bg-white rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto animate-in fade-in zoom-in-95 duration-200">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="bg-amber-100 p-2 rounded-full">
|
||||||
|
<AlertTriangle className="w-6 h-6 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-bold text-slate-900">
|
||||||
|
Before You Create a Petition
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 text-slate-700">
|
||||||
|
<p>
|
||||||
|
Please familiarize yourself with the laws and regulations for
|
||||||
|
drafting a petition:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://majlis.gov.mv/en/pes/petitions"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 text-blue-600 hover:text-blue-700 font-medium"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
majlis.gov.mv/en/pes/petitions
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
|
<p className="text-red-700 font-medium">
|
||||||
|
If you skip this step, there is a 100% chance we will reject
|
||||||
|
hosting your petition.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-50 rounded-lg p-4 space-y-3">
|
||||||
|
<p className="font-semibold text-slate-800">Important Rules (TLDR):</p>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="text-red-500 font-bold">1.</span>
|
||||||
|
<span>
|
||||||
|
You <strong>cannot</strong> mention people or businesses
|
||||||
|
directly in your petition.
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="text-red-500 font-bold">2.</span>
|
||||||
|
<span>
|
||||||
|
You <strong>cannot</strong> petition for something that only
|
||||||
|
benefits yourself.
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="text-red-500 font-bold">3.</span>
|
||||||
|
<span>
|
||||||
|
<strong>No anonymous petitions.</strong> Your Name and NID
|
||||||
|
will appear publicly on the petition. The submitter's
|
||||||
|
identity is always visible.
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex flex-col gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onAccept}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
I Understand, Continue
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="https://majlis.gov.mv/en/pes/petitions"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="w-full bg-slate-100 hover:bg-slate-200 text-slate-700 font-medium py-3 px-6 rounded-lg transition-colors text-center"
|
||||||
|
>
|
||||||
|
Read Full Guidelines First
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreatePetitionPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [showGuidelines, setShowGuidelines] = useState(true);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState<{ slug: string } | null>(null);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<PetitionFormData>({
|
||||||
|
slug: "",
|
||||||
|
nameDhiv: "",
|
||||||
|
nameEng: "",
|
||||||
|
startDate: formatDateForInput(new Date()),
|
||||||
|
authorName: "",
|
||||||
|
authorNid: "",
|
||||||
|
petitionBodyDhiv: "",
|
||||||
|
petitionBodyEng: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatDateForInput(date: Date): string {
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const year = date.getFullYear();
|
||||||
|
return `${day}-${month}-${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateSlug = () => {
|
||||||
|
const slug = formData.nameEng
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9\s-]/g, "")
|
||||||
|
.replace(/\s+/g, "-")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
.trim();
|
||||||
|
setFormData((prev) => ({ ...prev, slug }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await submitPetition(formData);
|
||||||
|
setSuccess({ slug: result.slug });
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to submit petition");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-50/50 p-4 md:p-8 font-sans">
|
||||||
|
<div className="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-slate-100 p-6 md:p-10">
|
||||||
|
<div className="text-center space-y-6">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="bg-green-100 p-4 rounded-full">
|
||||||
|
<CheckCircle className="w-12 h-12 text-green-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold text-slate-900">
|
||||||
|
Petition Created Successfully!
|
||||||
|
</h1>
|
||||||
|
<p className="text-slate-600">
|
||||||
|
Your petition has been submitted and is now live.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/Petition/${success.slug}`)}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
View Petition
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSuccess(null);
|
||||||
|
setFormData({
|
||||||
|
slug: "",
|
||||||
|
nameDhiv: "",
|
||||||
|
nameEng: "",
|
||||||
|
startDate: formatDateForInput(new Date()),
|
||||||
|
authorName: "",
|
||||||
|
authorNid: "",
|
||||||
|
petitionBodyDhiv: "",
|
||||||
|
petitionBodyEng: "",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="bg-slate-100 hover:bg-slate-200 text-slate-700 font-medium py-3 px-6 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Create Another
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-50/50 p-4 md:p-8 font-sans">
|
||||||
|
{showGuidelines && (
|
||||||
|
<GuidelinesModal onAccept={() => setShowGuidelines(false)} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-slate-100 p-6 md:p-10 animate-in fade-in duration-500 slide-in-from-bottom-4">
|
||||||
|
<div className="flex items-center gap-3 mb-8">
|
||||||
|
<div className="bg-blue-100 p-3 rounded-full">
|
||||||
|
<FileText className="w-6 h-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold text-slate-900">
|
||||||
|
Create a New Petition
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3">
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-red-700">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Petition Names */}
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
Petition Name (English) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="nameEng"
|
||||||
|
value={formData.nameEng}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={() => !formData.slug && generateSlug()}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||||
|
placeholder="Enter petition title in English"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
Petition Name (Dhivehi) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="nameDhiv"
|
||||||
|
value={formData.nameDhiv}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
dir="rtl"
|
||||||
|
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors font-dhivehi"
|
||||||
|
placeholder="ދިވެހި ނަން ލިޔުއްވާ"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slug */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
URL Slug *
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="slug"
|
||||||
|
value={formData.slug}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
pattern="[a-z0-9-]+"
|
||||||
|
className="flex-1 min-w-0 px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||||
|
placeholder="my-petition-name"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={generateSlug}
|
||||||
|
className="px-4 py-3 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg transition-colors text-sm whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Generate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 mt-1 break-all">
|
||||||
|
URL: /Petition/{formData.slug || "your-slug"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Start Date */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
Start Date *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="startDate"
|
||||||
|
value={formData.startDate}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
pattern="\d{2}-\d{2}-\d{4}"
|
||||||
|
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||||
|
placeholder="dd-MM-yyyy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Author Info */}
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
Author Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="authorName"
|
||||||
|
value={formData.authorName}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||||
|
placeholder="Enter author name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
Author National ID *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="authorNid"
|
||||||
|
value={formData.authorNid}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||||
|
placeholder="A123456"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Petition Bodies */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
Petition Body (English) *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="petitionBodyEng"
|
||||||
|
value={formData.petitionBodyEng}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
rows={6}
|
||||||
|
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors resize-y"
|
||||||
|
placeholder="Enter the full petition text in English..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
Petition Body (Dhivehi) *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="petitionBodyDhiv"
|
||||||
|
value={formData.petitionBodyDhiv}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
rows={6}
|
||||||
|
dir="rtl"
|
||||||
|
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors resize-y font-dhivehi"
|
||||||
|
placeholder="ޕެޓިޝަންގެ ތަފްސީލް ދިވެހި ބަހުން ލިޔުއްވާ..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-3 px-6 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
|
Creating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="w-5 h-5" />
|
||||||
|
Create Petition
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="text-center text-slate-500 text-sm mt-6 pb-4">
|
||||||
|
Powered by Mv Devs Union
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { FileText, PenLine } from "lucide-react";
|
||||||
|
|
||||||
|
export function HomePage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-50/50 p-4 md:p-8 font-sans">
|
||||||
|
<div className="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-slate-100 p-6 md:p-10 animate-in fade-in duration-500 slide-in-from-bottom-4">
|
||||||
|
<div className="text-center space-y-6">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="bg-blue-100 p-4 rounded-full">
|
||||||
|
<FileText className="w-12 h-12 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold text-slate-900">
|
||||||
|
Petition.com.mv
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-lg text-slate-600 max-w-xl mx-auto">
|
||||||
|
A platform for creating and signing petitions. Make your voice
|
||||||
|
heard on issues that matter to you and your community.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-slate-50 rounded-lg p-6 mt-8">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-800 mb-2">
|
||||||
|
Looking for a petition?
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-600">
|
||||||
|
Use the direct link shared with you to view and sign a petition.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4">
|
||||||
|
<Link
|
||||||
|
to="/CreatePetition"
|
||||||
|
className="inline-flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<PenLine className="w-5 h-5" />
|
||||||
|
Create a Petition
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="text-center text-slate-500 text-sm mt-6 pb-4">
|
||||||
|
Powered by Mv Devs Union
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { useState, useRef } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { usePetition } from "@/hooks/usePetition";
|
||||||
|
import { useLanguage } from "@/hooks/useLanguage";
|
||||||
|
import { submitSignature } from "@/lib/api";
|
||||||
|
import { LanguageSwitcher } from "@/components/layout/LanguageSwitcher";
|
||||||
|
import { LoadingState } from "@/components/layout/LoadingState";
|
||||||
|
import { ErrorState } from "@/components/layout/ErrorState";
|
||||||
|
import { PetitionHeader } from "@/components/petition/PetitionHeader";
|
||||||
|
import { AuthorCard } from "@/components/petition/AuthorCard";
|
||||||
|
import { PetitionBody } from "@/components/petition/PetitionBody";
|
||||||
|
import { SignatureForm } from "@/components/signature/SignatureForm";
|
||||||
|
import { TweetModal } from "@/components/TweetModal";
|
||||||
|
import { PenLine } from "lucide-react";
|
||||||
|
|
||||||
|
export function PetitionPage() {
|
||||||
|
const { slug } = useParams<{ slug: string }>();
|
||||||
|
const { petition, loading, error, refetch } = usePetition(slug ?? null);
|
||||||
|
const { language, setLanguage } = useLanguage();
|
||||||
|
const [showTweetModal, setShowTweetModal] = useState(false);
|
||||||
|
const signatureFormRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToSignForm = () => {
|
||||||
|
signatureFormRef.current?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "start",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (data: {
|
||||||
|
name: string;
|
||||||
|
idCard: string;
|
||||||
|
signature: string;
|
||||||
|
turnstileToken: string;
|
||||||
|
}) => {
|
||||||
|
if (!petition?.id) throw new Error("No petition ID");
|
||||||
|
|
||||||
|
await submitSignature(petition.id, data);
|
||||||
|
|
||||||
|
// Show tweet modal after successful submission
|
||||||
|
setShowTweetModal(true);
|
||||||
|
|
||||||
|
// Refresh petition data to update signature count
|
||||||
|
setTimeout(() => {
|
||||||
|
refetch();
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// No slug in URL
|
||||||
|
if (!slug) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background p-5">
|
||||||
|
<div className="max-w-4xl mx-auto bg-card rounded-lg shadow-lg p-10">
|
||||||
|
<ErrorState message="No petition found. Please use a valid petition URL." />
|
||||||
|
</div>
|
||||||
|
<footer className="text-center text-slate-500 text-sm mt-6 pb-4">
|
||||||
|
Powered by Mv Devs Union
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-50/50 p-4 md:p-8 font-sans">
|
||||||
|
<div className="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-slate-100 p-6 md:p-10 animate-in fade-in duration-500 slide-in-from-bottom-4">
|
||||||
|
{loading ? (
|
||||||
|
<div className="min-h-[400px] flex flex-col justify-center">
|
||||||
|
<LoadingState language={language} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{error && <ErrorState message={error} />}
|
||||||
|
|
||||||
|
{petition && (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<LanguageSwitcher
|
||||||
|
language={language}
|
||||||
|
onLanguageChange={setLanguage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PetitionHeader petition={petition} language={language} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={scrollToSignForm}
|
||||||
|
className={`w-full flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors ${language === "dv" ? "flex-row-reverse dhivehi" : ""}`}
|
||||||
|
>
|
||||||
|
<PenLine className="w-5 h-5" />
|
||||||
|
{language === "dv" ? "މިހާރު ސޮއި ކުރައްވާ" : "Sign Now"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AuthorCard
|
||||||
|
author={petition.authorDetails}
|
||||||
|
language={language}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PetitionBody
|
||||||
|
bodyEng={petition.petitionBodyEng}
|
||||||
|
bodyDhiv={petition.petitionBodyDhiv}
|
||||||
|
language={language}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div ref={signatureFormRef}>
|
||||||
|
<SignatureForm language={language} onSubmit={handleSubmit} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TweetModal
|
||||||
|
open={showTweetModal}
|
||||||
|
onClose={() => setShowTweetModal(false)}
|
||||||
|
petition={petition}
|
||||||
|
language={language}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="text-center text-slate-500 text-sm mt-6 pb-4">
|
||||||
|
Powered by Mv Devs Union
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ export interface Author {
|
|||||||
|
|
||||||
export interface PetitionDetails {
|
export interface PetitionDetails {
|
||||||
id: string;
|
id: string;
|
||||||
|
slug: string;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
nameDhiv: string;
|
nameDhiv: string;
|
||||||
nameEng: string;
|
nameEng: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user