create petitions

added support to create petitions from the website
This commit is contained in:
fISHIE
2026-01-28 15:22:03 +05:00
parent 80242d7621
commit 327dadff22
14 changed files with 715 additions and 1336 deletions

View File

@@ -20,6 +20,7 @@
"marked": "^17.0.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0",
"react-signature-canvas": "^1.1.0-alpha.2",
"tailwind-merge": "^3.4.0"
},
@@ -2804,6 +2805,18 @@
"dev": true,
"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": {
"version": "7.0.6",
"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": {
"version": "1.1.0-alpha.2",
"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"
}
},
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -23,6 +23,7 @@
"marked": "^17.0.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0",
"react-signature-canvas": "^1.1.0-alpha.2",
"tailwind-merge": "^3.4.0"
},

View File

@@ -1,127 +1,17 @@
import { useState, useRef } from "react";
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";
function getPetitionIdFromUrl(): string | null {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get("id");
return id;
}
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { HomePage } from "@/pages/HomePage";
import { PetitionPage } from "@/pages/PetitionPage";
import { CreatePetitionPage } from "@/pages/CreatePetitionPage";
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 (
<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>
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/Petition/:slug" element={<PetitionPage />} />
<Route path="/CreatePetition" element={<CreatePetitionPage />} />
</Routes>
</BrowserRouter>
);
}

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import type { PetitionDetails } from "@/types/petition";
import { fetchPetition, getDummyPetition } from "@/lib/api";
import { fetchPetitionBySlug, getDummyPetition } from "@/lib/api";
interface UsePetitionResult {
petition: PetitionDetails | null;
@@ -9,15 +9,15 @@ interface UsePetitionResult {
refetch: () => void;
}
export function usePetition(petitionId: string | null): UsePetitionResult {
export function usePetition(slug: string | null): UsePetitionResult {
const [petition, setPetition] = useState<PetitionDetails | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const loadPetition = useCallback(async () => {
if (!petitionId) {
if (!slug) {
setLoading(false);
setError("No petition ID provided");
setError("No petition slug provided");
return;
}
@@ -25,7 +25,7 @@ export function usePetition(petitionId: string | null): UsePetitionResult {
setError(null);
try {
const data = await fetchPetition(petitionId);
const data = await fetchPetitionBySlug(slug);
setPetition(data);
} catch (err) {
console.warn(
@@ -36,11 +36,11 @@ export function usePetition(petitionId: string | null): UsePetitionResult {
setError(
"Failed to load petition from server — showing dummy data for development.",
);
setPetition(getDummyPetition(petitionId));
setPetition(getDummyPetition(slug));
} finally {
setLoading(false);
}
}, [petitionId]);
}, [slug]);
useEffect(() => {
loadPetition();

View File

@@ -17,6 +17,69 @@ export async function fetchPetition(
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(
petitionId: string,
submission: SignatureSubmission,
@@ -39,9 +102,10 @@ export async function submitSignature(
}
// Dummy petition for development when API is not available
export function getDummyPetition(petitionId: string): PetitionDetails {
export function getDummyPetition(slug: string): PetitionDetails {
return {
id: petitionId || "dev-petition",
id: "dev-petition-id",
slug: slug || "demo-petition",
nameEng: "Demo Petition: Improve Local Services",
nameDhiv: "Demo Petition",
startDate: new Date().toLocaleDateString(),

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -5,6 +5,7 @@ export interface Author {
export interface PetitionDetails {
id: string;
slug: string;
startDate: string;
nameDhiv: string;
nameEng: string;