diff --git a/src/App.tsx b/src/App.tsx index 18daf2e..2ebfbca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import Index from "./pages/Index"; +import Testimonies from "./pages/Testimonies"; import NotFound from "./pages/NotFound"; const queryClient = new QueryClient(); @@ -16,6 +17,7 @@ const App = () => ( } /> + } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> diff --git a/src/components/PetitionForm.tsx b/src/components/PetitionForm.tsx index 7d1e52e..18a6865 100644 --- a/src/components/PetitionForm.tsx +++ b/src/components/PetitionForm.tsx @@ -3,6 +3,14 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { toast } from "sonner"; +import { supabase } from "@/integrations/supabase/client"; +import { z } from "zod"; + +const petitionSchema = z.object({ + name: z.string().trim().min(1, "Name is required").max(100, "Name must be less than 100 characters"), + email: z.string().trim().email("Invalid email address").max(255, "Email must be less than 255 characters"), + comment: z.string().trim().max(1000, "Comment must be less than 1000 characters").optional(), +}); export const PetitionForm = () => { const [formData, setFormData] = useState({ @@ -10,12 +18,42 @@ export const PetitionForm = () => { email: "", comment: "", }); + const [isSubmitting, setIsSubmitting] = useState(false); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // In a real application, this would submit to a backend - toast.success("Thank you for signing! Your voice matters."); - setFormData({ name: "", email: "", comment: "" }); + + // Validate input + try { + petitionSchema.parse(formData); + } catch (error) { + if (error instanceof z.ZodError) { + toast.error(error.errors[0].message); + return; + } + } + + setIsSubmitting(true); + + try { + const { error } = await supabase + .from('petition_signatures') + .insert([{ + name: formData.name.trim(), + email: formData.email.trim(), + comment: formData.comment.trim() || null, + }]); + + if (error) throw error; + + toast.success("Thank you for signing! Your voice matters."); + setFormData({ name: "", email: "", comment: "" }); + } catch (error) { + console.error('Error signing petition:', error); + toast.error("Failed to submit signature. Please try again."); + } finally { + setIsSubmitting(false); + } }; return ( @@ -50,9 +88,10 @@ export const PetitionForm = () => { ); diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 5997274..1166828 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -14,7 +14,30 @@ export type Database = { } public: { Tables: { - [_ in never]: never + petition_signatures: { + Row: { + comment: string | null + created_at: string + email: string + id: string + name: string + } + Insert: { + comment?: string | null + created_at?: string + email: string + id?: string + name: string + } + Update: { + comment?: string | null + created_at?: string + email?: string + id?: string + name?: string + } + Relationships: [] + } } Views: { [_ in never]: never diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 0eecaab..31cacaf 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -2,10 +2,48 @@ import { Button } from "@/components/ui/button"; import { TestimonialCard } from "@/components/TestimonialCard"; import { PetitionForm } from "@/components/PetitionForm"; import { StatCard } from "@/components/StatCard"; -import { Home, Users, Car, AlertTriangle } from "lucide-react"; +import { Home, Users, Car, AlertTriangle, MessageSquare } from "lucide-react"; import carparkHero from "@/assets/carpark-hero.jpg"; +import { useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { supabase } from "@/integrations/supabase/client"; const Index = () => { + const navigate = useNavigate(); + const [signatureCount, setSignatureCount] = useState(0); + + useEffect(() => { + fetchSignatureCount(); + + // Set up realtime subscription for signature count + const channel = supabase + .channel('signature-count') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'petition_signatures' + }, + () => { + setSignatureCount(prev => prev + 1); + } + ) + .subscribe(); + + return () => { + supabase.removeChannel(channel); + }; + }, []); + + const fetchSignatureCount = async () => { + const { count } = await supabase + .from('petition_signatures') + .select('*', { count: 'exact', head: true }); + + setSignatureCount(count || 0); + }; + const testimonials = [ { name: "Sarah Mitchell", @@ -116,11 +154,21 @@ const Index = () => {

Real stories from real residents affected by this decision

-
+
{testimonials.map((testimonial, index) => ( ))}
+
+ +
diff --git a/src/pages/Testimonies.tsx b/src/pages/Testimonies.tsx new file mode 100644 index 0000000..7595a68 --- /dev/null +++ b/src/pages/Testimonies.tsx @@ -0,0 +1,136 @@ +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { TestimonialCard } from "@/components/TestimonialCard"; +import { supabase } from "@/integrations/supabase/client"; +import { ArrowLeft, Users } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { formatDistanceToNow } from "date-fns"; + +interface Signature { + id: string; + name: string; + comment: string | null; + created_at: string; +} + +const Testimonies = () => { + const [signatures, setSignatures] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [isLoading, setIsLoading] = useState(true); + const navigate = useNavigate(); + + useEffect(() => { + fetchSignatures(); + + // Set up realtime subscription for new signatures + const channel = supabase + .channel('schema-db-changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'petition_signatures' + }, + (payload) => { + const newSignature = payload.new as Signature; + setSignatures(prev => [newSignature, ...prev]); + setTotalCount(prev => prev + 1); + } + ) + .subscribe(); + + return () => { + supabase.removeChannel(channel); + }; + }, []); + + const fetchSignatures = async () => { + try { + // Get total count + const { count } = await supabase + .from('petition_signatures') + .select('*', { count: 'exact', head: true }); + + setTotalCount(count || 0); + + // Get signatures with comments + const { data, error } = await supabase + .from('petition_signatures') + .select('*') + .not('comment', 'is', null) + .order('created_at', { ascending: false }); + + if (error) throw error; + setSignatures(data || []); + } catch (error) { + console.error('Error fetching signatures:', error); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* Header */} +
+
+ +
+ + {totalCount} Signatures +
+
+
+ + {/* Content */} +
+
+

+ Community Testimonies +

+

+ Real stories from residents affected by the closure of Victoria Way Carpark +

+
+ + {isLoading ? ( +
+

Loading testimonies...

+
+ ) : signatures.length === 0 ? ( +
+

No testimonies yet. Be the first to share your story!

+
+ ) : ( +
+ {signatures.map((signature) => ( + + ))} +
+ )} +
+ + {/* Footer */} + +
+ ); +}; + +export default Testimonies; diff --git a/supabase/migrations/20251024125608_c4917347-f205-4a51-82d8-0b6d8f9ec6ec.sql b/supabase/migrations/20251024125608_c4917347-f205-4a51-82d8-0b6d8f9ec6ec.sql new file mode 100644 index 0000000..4e3d761 --- /dev/null +++ b/supabase/migrations/20251024125608_c4917347-f205-4a51-82d8-0b6d8f9ec6ec.sql @@ -0,0 +1,32 @@ +-- Create petition signatures table +CREATE TABLE public.petition_signatures ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL, + comment TEXT, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- Enable Row Level Security +ALTER TABLE public.petition_signatures ENABLE ROW LEVEL SECURITY; + +-- Allow anyone to insert signatures (public petition) +CREATE POLICY "Anyone can sign the petition" +ON public.petition_signatures +FOR INSERT +TO anon, authenticated +WITH CHECK (true); + +-- Allow anyone to view signatures and testimonies +CREATE POLICY "Anyone can view signatures" +ON public.petition_signatures +FOR SELECT +TO anon, authenticated +USING (true); + +-- Create index for faster queries by date +CREATE INDEX idx_petition_signatures_created_at +ON public.petition_signatures(created_at DESC); + +-- Enable realtime for live signature updates +ALTER PUBLICATION supabase_realtime ADD TABLE public.petition_signatures; \ No newline at end of file