diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ad9f6bf..76f3691 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,7 +5,8 @@ "Bash(docker compose:*)", "Bash(chmod:*)", "Bash(tree:*)", - "Bash(./ci-helper:*)" + "Bash(./ci-helper:*)", + "Bash(npm run build:*)" ] } } diff --git a/Frontend/README.md b/Frontend/README.md deleted file mode 100644 index 5a6d952..0000000 --- a/Frontend/README.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/Frontend/fonts/utheem.ttf b/Frontend/fonts/utheem.ttf deleted file mode 100644 index 2f244dc..0000000 Binary files a/Frontend/fonts/utheem.ttf and /dev/null differ diff --git a/Frontend/index.html b/Frontend/index.html deleted file mode 100644 index 10dc094..0000000 --- a/Frontend/index.html +++ /dev/null @@ -1,701 +0,0 @@ - - - - - - Petition Details - - - - - - - - -
-
Loading petition...
- - -
- - - - - - - diff --git a/Frontend/style.css b/Frontend/style.css deleted file mode 100644 index d0375c7..0000000 --- a/Frontend/style.css +++ /dev/null @@ -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; - } -} diff --git a/frontend-react/package-lock.json b/frontend-react/package-lock.json index 2ead694..35222a4 100644 --- a/frontend-react/package-lock.json +++ b/frontend-react/package-lock.json @@ -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", diff --git a/frontend-react/package.json b/frontend-react/package.json index 2b8b4bf..3881eda 100644 --- a/frontend-react/package.json +++ b/frontend-react/package.json @@ -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" }, diff --git a/frontend-react/src/App.tsx b/frontend-react/src/App.tsx index 99ec712..335d2b3 100644 --- a/frontend-react/src/App.tsx +++ b/frontend-react/src/App.tsx @@ -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(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 ( -
-
- -
-
- Powered by Mv Devs Union -
-
- ); - } - return ( -
-
- {loading ? ( -
- -
- ) : ( - <> - {error && } - - {petition && ( -
- - - - - - - - - - -
- -
- - setShowTweetModal(false)} - petition={petition} - language={language} - /> -
- )} - - )} -
- -
- Powered by Mv Devs Union -
-
+ + + } /> + } /> + } /> + + ); } diff --git a/frontend-react/src/hooks/usePetition.ts b/frontend-react/src/hooks/usePetition.ts index 842eb3a..cd42dab 100644 --- a/frontend-react/src/hooks/usePetition.ts +++ b/frontend-react/src/hooks/usePetition.ts @@ -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(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(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(); diff --git a/frontend-react/src/lib/api.ts b/frontend-react/src/lib/api.ts index ec6e2d5..2d206c5 100644 --- a/frontend-react/src/lib/api.ts +++ b/frontend-react/src/lib/api.ts @@ -17,6 +17,69 @@ export async function fetchPetition( return response.json(); } +export async function fetchPetitionBySlug( + slug: string, +): Promise { + 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 { + 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(), diff --git a/frontend-react/src/pages/CreatePetitionPage.tsx b/frontend-react/src/pages/CreatePetitionPage.tsx new file mode 100644 index 0000000..a762f76 --- /dev/null +++ b/frontend-react/src/pages/CreatePetitionPage.tsx @@ -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 ( +
+
+
+
+
+ +
+

+ Before You Create a Petition +

+
+ +
+

+ Please familiarize yourself with the laws and regulations for + drafting a petition: +

+ + + + majlis.gov.mv/en/pes/petitions + + +
+

+ If you skip this step, there is a 100% chance we will reject + hosting your petition. +

+
+ +
+

Important Rules (TLDR):

+
    +
  • + 1. + + You cannot mention people or businesses + directly in your petition. + +
  • +
  • + 2. + + You cannot petition for something that only + benefits yourself. + +
  • +
  • + 3. + + No anonymous petitions. Your Name and NID + will appear publicly on the petition. The submitter's + identity is always visible. + +
  • +
+
+
+ +
+ + + Read Full Guidelines First + +
+
+
+
+ ); +} + +export function CreatePetitionPage() { + const navigate = useNavigate(); + const [showGuidelines, setShowGuidelines] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState<{ slug: string } | null>(null); + + const [formData, setFormData] = useState({ + 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 + ) => { + 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 ( +
+
+
+
+
+ +
+
+

+ Petition Created Successfully! +

+

+ Your petition has been submitted and is now live. +

+
+ + +
+
+
+
+ ); + } + + return ( +
+ {showGuidelines && ( + setShowGuidelines(false)} /> + )} + +
+
+
+ +
+

+ Create a New Petition +

+
+ + {error && ( +
+ +

{error}

+
+ )} + +
+ {/* Petition Names */} +
+
+ + !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" + /> +
+
+ + +
+
+ + {/* Slug */} +
+ +
+ + +
+

+ URL: /Petition/{formData.slug || "your-slug"} +

+
+ + {/* Start Date */} +
+ + +
+ + {/* Author Info */} +
+
+ + +
+
+ + +
+
+ + {/* Petition Bodies */} +
+ +