diff --git a/public/hero.jpeg b/public/hero.jpeg deleted file mode 100644 index ca038be..0000000 Binary files a/public/hero.jpeg and /dev/null differ diff --git a/public/logo.jpg b/public/logo.jpg new file mode 100644 index 0000000..1f37c17 Binary files /dev/null and b/public/logo.jpg differ diff --git a/src/components/RSVP.tsx b/src/components/RSVP.tsx index 7b72609..b9144c6 100644 --- a/src/components/RSVP.tsx +++ b/src/components/RSVP.tsx @@ -44,7 +44,11 @@ const RSVPForm = () => { headers: { "Content-Type": "application/json", }, - body: JSON.stringify(formData), + body: JSON.stringify({ + ...formData, + attending: formData.attending === true, // Ensure it's a boolean + timestamp: new Date().toISOString(), + }), signal: controller.signal, }); @@ -242,7 +246,7 @@ const RSVPForm = () => { ) : formData.attending ? ( "Yes, I'll Be There!" ) : formData.attending === false ? ( - "Send My Regrets" + "Submit RSVP" ) : ( "Submit RSVP" )} diff --git a/src/components/RSVPManager.tsx b/src/components/RSVPManager.tsx index 5b7cbf5..e6aea09 100644 --- a/src/components/RSVPManager.tsx +++ b/src/components/RSVPManager.tsx @@ -33,6 +33,31 @@ const RSVPManager = () => { } }; + const handleDeleteRSVP = async (name: string, timestamp: string) => { + if (!confirm(`Are you sure you want to delete the RSVP from ${name}?`)) { + return; + } + + try { + const response = await fetchWithAuth(`/api/rsvp`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name, timestamp }), + }); + + if (!response.ok) { + throw new Error(`Failed to delete RSVP: ${response.statusText}`); + } + + await fetchRSVPList(); + } catch (error: any) { + setError(error.message); + console.error("Error deleting RSVP:", error); + } + }; + useEffect(() => { fetchRSVPList(); @@ -57,30 +82,27 @@ const RSVPManager = () => { const attending = rsvpList.filter(rsvp => rsvp.attending === true); const notAttending = rsvpList.filter(rsvp => rsvp.attending === false); - const noResponse = rsvpList.filter(rsvp => rsvp.attending === undefined || rsvp.attending === null); return ( -
-
+
+
-
Total RSVPs
+
Total Responses
{rsvpList.length}
Attending
{attending.length}
+
Will be there
Not Attending
{notAttending.length}
-
-
-
No Response
-
{noResponse.length}
+
Can't make it
-
+
@@ -88,7 +110,8 @@ const RSVPManager = () => { - + + @@ -96,19 +119,29 @@ const RSVPManager = () => { + + + + - - - ))} diff --git a/src/components/RegistryList.tsx b/src/components/RegistryList.tsx index e7d5aab..f4b1bce 100644 --- a/src/components/RegistryList.tsx +++ b/src/components/RegistryList.tsx @@ -4,10 +4,11 @@ import { fetchWithAuth, getAuthToken } from "../utils/auth-client"; const RegistryList = () => { const [registryItems, setRegistryItems] = useState([]); + const [claimedItems, setClaimedItems] = useState>(new Set()); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [claimantName, setClaimantName] = useState(""); - const [claimedItems, setClaimedItems] = useState>(new Set()); + const [showClaimants, setShowClaimants] = useState>(new Set()); const fetchRegistryItems = async () => { // Don't try to fetch if we don't have a token yet @@ -52,7 +53,7 @@ const RegistryList = () => { return () => document.removeEventListener("auth-success", handleAuthSuccess); }, []); - const handleItemClick = (itemId: string) => { + const handleCheckboxChange = (itemId: string) => { const newClaimedItems = new Set(claimedItems); if (newClaimedItems.has(itemId)) { newClaimedItems.delete(itemId); @@ -62,6 +63,16 @@ const RegistryList = () => { setClaimedItems(newClaimedItems); }; + const toggleClaimantVisibility = (itemId: string) => { + const newShowClaimants = new Set(showClaimants); + if (newShowClaimants.has(itemId)) { + newShowClaimants.delete(itemId); + } else { + newShowClaimants.add(itemId); + } + setShowClaimants(newShowClaimants); + }; + const handleSubmit = async () => { if (!claimantName.trim()) { setError("Please enter your name before claiming items"); @@ -109,87 +120,108 @@ const RegistryList = () => { } if (error) { + if (error === "No authentication token found") { + return
Initializing...
; + } return
Error: {error}
; } - const availableItems = registryItems.filter((item) => !item.taken); - const claimedItemsArray = registryItems.filter((item) => item.taken); - return (
- {availableItems.length > 0 && ( - <> -
- setClaimantName(e.target.value)} - placeholder="Enter your name" - className="input input-bordered w-full max-w-xs" - /> -
- -
- {availableItems.map((item) => ( -
handleItemClick(item.id)} - > -
-

{item.name}

+

Wedding Registry

+
+
Status Dietary Restrictions NotesTimestampResponse DateActions
{rsvp.name} -
- {rsvp.attending === true ? 'Attending' : - rsvp.attending === false ? 'Not Attending' : - 'No Response'} -
+ + {rsvp.attending ? 'Attending' : 'Not Attending'} + +
{rsvp.dietaryRestrictions && rsvp.dietaryRestrictions !== "undefined" ? rsvp.dietaryRestrictions : "-"}{rsvp.notes && rsvp.notes !== "undefined" ? rsvp.notes : "-"}{new Date(rsvp.timestamp).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} + {rsvp.attending ? (rsvp.dietaryRestrictions || "None") : "-"}{(rsvp.notes && rsvp.notes !== "undefined") ? rsvp.notes.trim() : "None"}{new Date(rsvp.timestamp).toLocaleString()}
+ + + + + + + + + + {registryItems.map((item) => ( + + + + + + ))} + +
ItemLinkStatusClaim
+
+
+ {item.name} + {item.taken && ( +
+ Taken + +
+ )} +
+ {item.taken && + item.claimedBy && + showClaimants.has(item.id) && ( +
+ Claimed by: {item.claimedBy} +
+ )} +
+
{item.link && ( e.stopPropagation()} > - View Item + View )} - - - ))} - - - {claimedItems.size > 0 && ( -
- -
- )} - - )} - - {claimedItemsArray.length > 0 && ( -
-

Already Claimed

-
- {claimedItemsArray.map((item) => ( -
-
-

{item.name}

-

- Claimed by: {item.claimedBy} -

-
-
+
{item.taken ? "Claimed" : "Available"} + {!item.taken ? ( + handleCheckboxChange(item.id)} + /> + ) : } +
+
+ {claimedItems.size > 0 && ( +
+
+ + setClaimantName(e.target.value)} + placeholder="Enter your name" + required + />
+
)}
diff --git a/src/components/RegistryManager.tsx b/src/components/RegistryManager.tsx index f3c9f89..bd11d52 100644 --- a/src/components/RegistryManager.tsx +++ b/src/components/RegistryManager.tsx @@ -88,6 +88,27 @@ const RegistryManager = () => { } }; + const handleReleaseClaim = async (id: string) => { + try { + const response = await fetchWithAuth(`/api/registry/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ taken: false, claimedBy: null }), + }); + + if (!response.ok) { + throw new Error(`Failed to release claim: ${response.statusText}`); + } + + await fetchRegistryItems(); + } catch (error: any) { + setError(error.message); + console.error("Error releasing claim:", error); + } + }; + const toggleShowClaimant = (id: string) => { const newShowClaimants = new Set(showClaimants); if (newShowClaimants.has(id)) { @@ -111,26 +132,32 @@ const RegistryManager = () => { return (
-
-
- setNewItem({ ...newItem, name: e.target.value })} - placeholder="Item name" - className="input input-bordered" - required - /> - setNewItem({ ...newItem, link: e.target.value })} - placeholder="Item link (optional)" - className="input input-bordered" - /> - + +
+
+ setNewItem({ ...newItem, name: e.target.value })} + placeholder="Item name" + className="input input-bordered w-full" + required + /> +
+
+ setNewItem({ ...newItem, link: e.target.value })} + placeholder="Item link (optional)" + className="input input-bordered w-full" + /> +
+
+ +
@@ -185,12 +212,22 @@ const RegistryManager = () => { )} - +
+ {item.taken && ( + + )} + +
))} diff --git a/src/pages/api/rsvp.ts b/src/pages/api/rsvp.ts index 15b45e9..6206dd6 100644 --- a/src/pages/api/rsvp.ts +++ b/src/pages/api/rsvp.ts @@ -4,13 +4,21 @@ import type { RSVPItem } from "../../lib/types"; import { createProtectedAPIRoute } from "../../utils/auth-middleware"; const objectsToCSV = (data: RSVPItem[]): string => { - const headers = ["name", "dietaryRestrictions", "notes", "timestamp"]; + const headers = ["name", "attending", "dietaryRestrictions", "notes", "timestamp"]; const csvRows = [headers.join(",")]; data.forEach((entry) => { const row = headers.map((header) => { - const field = String(entry[header as keyof RSVPItem]); - const escaped = field.replace(/"/g, '""'); + let field = entry[header as keyof RSVPItem]; + // Convert boolean to string for CSV + if (typeof field === 'boolean') { + field = field.toString(); + } + // Handle null/undefined + if (field === null || field === undefined) { + field = ''; + } + const escaped = String(field).replace(/"/g, '""'); return `"${escaped}"`; }); csvRows.push(row.join(",")); @@ -22,18 +30,14 @@ const objectsToCSV = (data: RSVPItem[]): string => { const csvToObjects = (csv: string): RSVPItem[] => { const lines = csv.split("\n"); const headers = lines[0].split(",").map((h) => h.trim()); + const hasAttendingColumn = headers.includes("attending"); return lines .slice(1) .filter((line) => line.trim()) .map((line) => { const values = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || []; - const entry: Partial = { - name: "", - dietaryRestrictions: "", - notes: "", - timestamp: "", - }; + const entry: Partial = {}; headers.forEach((header, index) => { let value = values[index] || ""; @@ -41,6 +45,13 @@ const csvToObjects = (csv: string): RSVPItem[] => { entry[header as keyof RSVPItem] = value; }); + // If the attending column doesn't exist in the CSV, assume they're attending if they provided dietary restrictions + if (!hasAttendingColumn) { + entry.attending = Boolean(entry.dietaryRestrictions); + } else if (typeof entry.attending === 'string') { + entry.attending = entry.attending.toLowerCase() === 'true'; + } + return entry as RSVPItem; }); }; @@ -84,38 +95,29 @@ const handleGet: APIRoute = async ({ request }) => { // POST: Submit a new RSVP (requires guest role) const handlePost: APIRoute = async ({ request }) => { - console.log("API endpoint hit - starting request processing"); - const headers = { - "Access-Control-Allow-Origin": "*", "Content-Type": "application/json", }; try { const FILE_KEY = "rsvp.csv"; - - console.log("Parsing request body"); const newRsvp = await request.json(); - console.log("Received RSVP data:", newRsvp); let existingRsvps: RSVPItem[] = []; - - console.log("Attempting to fetch existing RSVPs"); const fileContent = await getS3Data(FILE_KEY); if (fileContent) { existingRsvps = csvToObjects(fileContent); - console.log("Existing RSVPs loaded:", existingRsvps.length); } existingRsvps.push({ ...newRsvp, + attending: Boolean(newRsvp.attending), notes: newRsvp.notes || "", - timestamp: new Date().toISOString(), + dietaryRestrictions: newRsvp.dietaryRestrictions || "", + timestamp: newRsvp.timestamp || new Date().toISOString(), }); - console.log("Attempting to save updated RSVP list"); const csvContent = objectsToCSV(existingRsvps); - await putS3Data(FILE_KEY, csvContent, "text/csv"); return new Response(JSON.stringify({ success: true }), { @@ -128,7 +130,85 @@ const handlePost: APIRoute = async ({ request }) => { JSON.stringify({ success: false, error: error instanceof Error ? error.message : "Unknown error", - details: error instanceof Error ? error.stack : undefined, + }), + { + status: 500, + headers, + } + ); + } +}; + +// DELETE: Delete an RSVP (requires admin role) +const handleDelete: APIRoute = async ({ request }) => { + const headers = { + "Content-Type": "application/json", + }; + + try { + const FILE_KEY = "rsvp.csv"; + const { name, timestamp } = await request.json(); + + if (!name || !timestamp) { + return new Response( + JSON.stringify({ + success: false, + error: "Name and timestamp are required", + }), + { + status: 400, + headers, + } + ); + } + + const fileContent = await getS3Data(FILE_KEY); + if (!fileContent) { + return new Response( + JSON.stringify({ + success: false, + error: "No RSVPs found", + }), + { + status: 404, + headers, + } + ); + } + + const rsvpList = csvToObjects(fileContent); + + // Find exact match by name and timestamp + const updatedList = rsvpList.filter( + (rsvp) => !(rsvp.name === name && rsvp.timestamp === timestamp) + ); + + if (updatedList.length === rsvpList.length) { + return new Response( + JSON.stringify({ + success: false, + error: "RSVP not found", + }), + { + status: 404, + headers, + } + ); + } + + const csvContent = objectsToCSV(updatedList); + await putS3Data(FILE_KEY, csvContent, "text/csv"); + + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers, + }); + } catch (error) { + console.error("Error deleting RSVP:", error); + return new Response( + JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : "Unknown error", }), { status: 500, @@ -141,3 +221,4 @@ const handlePost: APIRoute = async ({ request }) => { // Export protected routes export const GET = createProtectedAPIRoute(handleGet, "admin"); export const POST = createProtectedAPIRoute(handlePost, "guest"); +export const DELETE = createProtectedAPIRoute(handleDelete, "admin"); diff --git a/src/pages/faq.astro b/src/pages/faq.astro index fb5185b..9494604 100644 --- a/src/pages/faq.astro +++ b/src/pages/faq.astro @@ -13,7 +13,7 @@ import Layout from "../layouts/Layout.astro";
- Dress Code? + Dress Code

@@ -27,7 +27,7 @@ import Layout from "../layouts/Layout.astro";

- Dietary Restrictions? + Dietary Restrictions

@@ -41,7 +41,7 @@ import Layout from "../layouts/Layout.astro";

- Alcohol? + Alcohol

@@ -54,7 +54,7 @@ import Layout from "../layouts/Layout.astro";

- Childcare? + Childcare

@@ -68,7 +68,7 @@ import Layout from "../layouts/Layout.astro";

- Gifts? + Gifts

@@ -99,7 +99,7 @@ import Layout from "../layouts/Layout.astro";

- Where Do I Contact You With All of My Concerns? + I have all these concerns, what do I do?
diff --git a/src/pages/index.astro b/src/pages/index.astro index 2126e3c..fc2ed2b 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -10,10 +10,10 @@ import SignIn from "../components/SignIn.tsx";