feat: fetching signatures from backend endpoint

This commit is contained in:
2025-11-10 22:36:54 +00:00
parent 4371b26423
commit f8712015c0
9 changed files with 91 additions and 33 deletions

View File

@ -1,4 +1,5 @@
import { ENV } from "./env"; import { ENV } from "./env";
import { getPetitions } from "./routes/get-petitions";
import { signPetition } from "./routes/sign-petition"; import { signPetition } from "./routes/sign-petition";
const CORS_HEADERS = { const CORS_HEADERS = {
@ -29,7 +30,8 @@ const server = Bun.serve({
port: ENV.PORT, port: ENV.PORT,
routes: { routes: {
"/health": new Response("alive!"), "/health": new Response("alive!"),
"/sign-petition": { "/sign": {
GET: withCors(getPetitions),
POST: withCors(signPetition), POST: withCors(signPetition),
OPTIONS: allowCors, OPTIONS: allowCors,
}, },

View File

@ -1,8 +1,19 @@
import { db } from "./database"; import { db } from "./database";
import { signaturesTable } from "./schema"; import { signaturesTable } from "./schema";
export const insertSignature = async (signature: typeof signaturesTable.$inferInsert): Promise<typeof signaturesTable.$inferSelect | undefined> => { export const insertSignature = async (
const [insertedSignature] = await db.insert(signaturesTable).values(signature).returning(); signature: typeof signaturesTable.$inferInsert,
): Promise<typeof signaturesTable.$inferSelect | undefined> => {
const [insertedSignature] = await db
.insert(signaturesTable)
.values(signature)
.returning();
return insertedSignature; return insertedSignature;
} };
export const getSignatures = async (): Promise<
Array<typeof signaturesTable.$inferSelect>
> => {
return db.select().from(signaturesTable);
};

View File

@ -0,0 +1,10 @@
import { signedPetitionArraySchema } from "types";
import { getSignatures } from "../models";
export const getPetitions = async (_: Request): Promise<Response> => {
const signatures = await getSignatures();
const parsedSignatures = signedPetitionArraySchema.parse(signatures);
return Response.json(parsedSignatures, { status: 200 });
};

View File

@ -34,12 +34,22 @@ export const PetitionForm = ({ compact = false }: PetitionFormProps) => {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (isSubmitting) {
return;
}
setIsSubmitting(true); setIsSubmitting(true);
await onSignPetition({ await onSignPetition({
email, email,
comment, comment,
name, name,
}); });
setName(undefined);
setEmail(undefined);
setComment(undefined);
setIsSubmitting(false); setIsSubmitting(false);
}; };

View File

@ -3,16 +3,33 @@ import { signedPetitionSchema, signPetitionSchema } from "types";
const backendUrl = import.meta.env.VITE_BACKEND_URL; const backendUrl = import.meta.env.VITE_BACKEND_URL;
export const signedPetitionWithParsedDate = signedPetitionSchema.extend({
createdAt: z.string().transform((date: string) => new Date(date)),
});
const signedPetitionSignatures = z.array(signedPetitionWithParsedDate);
export const getSignatures = async (): Promise<
z.infer<typeof signedPetitionSignatures>
> => {
const res = await fetch(`${backendUrl}/sign`);
const body = await res.json();
const validatedBody = signedPetitionSignatures.parse(body);
return validatedBody;
};
export const signPetition = async ( export const signPetition = async (
signature: z.infer<typeof signPetitionSchema>, signature: z.infer<typeof signPetitionSchema>,
): Promise<z.infer<typeof signedPetitionSchema>> => { ): Promise<z.infer<typeof signedPetitionWithParsedDate>> => {
const res = await fetch(`${backendUrl}/sign-petition`, { const res = await fetch(`${backendUrl}/sign`, {
method: "POST", method: "POST",
body: JSON.stringify(signature), body: JSON.stringify(signature),
}); });
const body = await res.json(); const body = await res.json();
const validatedBody = signedPetitionSchema.parse(body); const validatedBody = signedPetitionWithParsedDate.parse(body);
return validatedBody; return validatedBody;
}; };

View File

@ -11,7 +11,7 @@ import emptyBuildingParking from "@/assets/empty-building-parking.jpg";
import asahiBuilding from "@/assets/asahi-building.jpg"; import asahiBuilding from "@/assets/asahi-building.jpg";
import doubletreeHilton from "@/assets/doubletree-hilton.jpg"; import doubletreeHilton from "@/assets/doubletree-hilton.jpg";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useState } from "react"; import { usePetitions } from "@/state";
const STATIC_TESTEMONIES = [ const STATIC_TESTEMONIES = [
{ {
@ -36,7 +36,8 @@ const STATIC_TESTEMONIES = [
const Index = () => { const Index = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [signatureCount, setSignatureCount] = useState(0);
const { signatures } = usePetitions();
const scrollToPetition = () => { const scrollToPetition = () => {
document.getElementById("petition")?.scrollIntoView({ behavior: "smooth" }); document.getElementById("petition")?.scrollIntoView({ behavior: "smooth" });
@ -67,12 +68,13 @@ const Index = () => {
Sign the Petition Sign the Petition
</h2> </h2>
<p className="text-lg text-muted-foreground"> <p className="text-lg text-muted-foreground">
{signatureCount > 0 && ( {signatures.length > 0 && (
<span className="font-semibold text-accent"> <span className="font-semibold text-accent">
{signatureCount} people {signatures.length} people
</span> </span>
)} )}
{signatureCount > 0 ? " have" : "Be the first to"} signed so far {signatures.length > 0 ? " have" : "Be the first to"} signed so
far
</p> </p>
</div> </div>
<PetitionForm compact /> <PetitionForm compact />
@ -438,7 +440,8 @@ const Index = () => {
className="gap-2 bg-primary text-primary-foreground hover:bg-primary/90" className="gap-2 bg-primary text-primary-foreground hover:bg-primary/90"
> >
<MessageSquare className="w-5 h-5" /> <MessageSquare className="w-5 h-5" />
Read All {signatureCount > 0 && `${signatureCount} `}Testimonies Read All {signatures.length > 0 && `${signatures.length} `}
Testimonies
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -1,20 +1,16 @@
import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { TestimonialCard } from "@/components/TestimonialCard"; import { TestimonialCard } from "@/components/TestimonialCard";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { ArrowLeft, Users } from "lucide-react"; import { ArrowLeft, Users } from "lucide-react";
import { usePetitions } from "@/state";
interface Signature {
id: string;
name: string;
comment: string | null;
created_at: string;
}
const Testimonies = () => { const Testimonies = () => {
const [signatures, setSignatures] = useState<Signature[]>([]); const { signatures } = usePetitions();
const [totalCount, setTotalCount] = useState(0); console.log(signatures);
const totalCount = signatures.length;
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
@ -55,7 +51,7 @@ const Testimonies = () => {
key={signature.id} key={signature.id}
name={signature.name} name={signature.name}
comment={signature.comment || ""} comment={signature.comment || ""}
date={formatDistanceToNow(new Date(signature.created_at), { date={formatDistanceToNow(new Date(signature.createdAt), {
addSuffix: true, addSuffix: true,
})} })}
/> />

View File

@ -1,17 +1,22 @@
import { signPetition } from "@/network"; import {
getSignatures,
signedPetitionWithParsedDate,
signPetition,
} from "@/network";
import { import {
createContext, createContext,
ReactNode, ReactNode,
useCallback, useCallback,
useContext, useContext,
useEffect,
useState, useState,
} from "react"; } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { signedPetitionSchema, signPetitionSchema } from "types"; import { signPetitionSchema } from "types";
import { z } from "zod"; import { z } from "zod";
// submitted is used to determine if the signature was inserted correctly // submitted is used to determine if the signature was inserted correctly
type SignatureWithState = z.infer<typeof signedPetitionSchema> & { type SignatureWithState = z.infer<typeof signedPetitionWithParsedDate> & {
submitted: boolean; submitted: boolean;
}; };
@ -36,6 +41,12 @@ export const PetitionStateProvider = ({
[], [],
); );
useEffect(() => {
getSignatures().then((signatures) => {
setSignatures(signatures.map((s) => ({ ...s, submitted: true })));
});
}, []);
const onSignPetition = useCallback<PetitionStateType["onSignPetition"]>( const onSignPetition = useCallback<PetitionStateType["onSignPetition"]>(
async (signature) => { async (signature) => {
const eagerPetitionId = Date.now().toString(); const eagerPetitionId = Date.now().toString();
@ -43,7 +54,7 @@ export const PetitionStateProvider = ({
setSignatures((petitions) => [ setSignatures((petitions) => [
{ {
id: eagerPetitionId, id: eagerPetitionId,
createdAt: new Date().toISOString(), createdAt: new Date(),
submitted: false, submitted: false,
...signature, ...signature,
}, },
@ -68,10 +79,6 @@ export const PetitionStateProvider = ({
return [...petitions]; return [...petitions];
}); });
toast.error(
"Sorry, had a problem inserting your signature. Please try again.",
);
} catch (err) { } catch (err) {
console.warn(err); console.warn(err);

View File

@ -10,3 +10,5 @@ export const signedPetitionSchema = signPetitionSchema.extend({
id: z.uuid(), id: z.uuid(),
createdAt: z.date().transform((date: Date) => date.toISOString()), createdAt: z.date().transform((date: Date) => date.toISOString()),
}); });
export const signedPetitionArraySchema = z.array(signedPetitionSchema);