mirror of
https://github.com/MvDevsUnion/WPetition.git
synced 2026-03-01 05:20:36 +00:00
create petitions
added support to create petitions from the website
This commit is contained in:
54
frontend-react/package-lock.json
generated
54
frontend-react/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
400
frontend-react/src/pages/CreatePetitionPage.tsx
Normal file
400
frontend-react/src/pages/CreatePetitionPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
50
frontend-react/src/pages/HomePage.tsx
Normal file
50
frontend-react/src/pages/HomePage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
123
frontend-react/src/pages/PetitionPage.tsx
Normal file
123
frontend-react/src/pages/PetitionPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export interface Author {
|
||||
|
||||
export interface PetitionDetails {
|
||||
id: string;
|
||||
slug: string;
|
||||
startDate: string;
|
||||
nameDhiv: string;
|
||||
nameEng: string;
|
||||
|
||||
Reference in New Issue
Block a user