From 6b01c165a99c8bfa280d9689f20b865cd33d65bf Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Fri, 7 Feb 2025 02:14:36 -0600 Subject: [PATCH] Wonfidence --- .env.example | 3 +- package.json | 3 +- pnpm-lock.yaml | 9 ++ src/components/RSVP.tsx | 9 +- src/components/RegistryList.tsx | 152 ++++++++++++++++++++++++ src/components/RegistryManager.tsx | 184 +++++++++++++++++++++++++++++ src/components/SignIn.tsx | 32 +++-- src/components/SignOut.tsx | 33 ++++++ src/lib/s3.ts | 66 +++++++++++ src/lib/types.ts | 12 ++ src/pages/api/auth.ts | 33 +++++- src/pages/api/registry/[id].ts | 123 +++++++++++++++++++ src/pages/api/registry/index.ts | 60 ++++++++++ src/pages/api/rsvp.ts | 100 ++++------------ src/pages/index.astro | 5 +- src/pages/registry/admin.astro | 55 +++++++++ src/pages/registry/index.astro | 47 ++++++++ src/pages/rsvp.astro | 4 +- 18 files changed, 828 insertions(+), 102 deletions(-) create mode 100644 src/components/RegistryList.tsx create mode 100644 src/components/RegistryManager.tsx create mode 100644 src/components/SignOut.tsx create mode 100644 src/lib/s3.ts create mode 100644 src/lib/types.ts create mode 100644 src/pages/api/registry/[id].ts create mode 100644 src/pages/api/registry/index.ts create mode 100644 src/pages/registry/admin.astro create mode 100644 src/pages/registry/index.astro diff --git a/.env.example b/.env.example index 6818395..46cbbae 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,5 @@ S3_HOST=s3.cool.site S3_ACCESS_KEY=your_access_key_here S3_SECRET_KEY=your_secret_key_here S3_BUCKET_NAME=your_bucket_name -SECRET_CODE=super_secret_code \ No newline at end of file +SECRET_CODE=super_secret_code +ADMIN_CODE=super_secret_code \ No newline at end of file diff --git a/package.json b/package.json index cbc99fd..5d1daf8 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "tailwindcss": "^4.0.3", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "uuid": "^11.0.5" }, "devDependencies": { "daisyui": "5.0.0-beta.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d35ea5..7075e86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: typescript: specifier: ^5.7.3 version: 5.7.3 + uuid: + specifier: ^11.0.5 + version: 11.0.5 devDependencies: daisyui: specifier: 5.0.0-beta.6 @@ -2236,6 +2239,10 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uuid@11.0.5: + resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==} + hasBin: true + uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true @@ -5202,6 +5209,8 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uuid@11.0.5: {} + uuid@9.0.1: {} vfile-location@5.0.3: diff --git a/src/components/RSVP.tsx b/src/components/RSVP.tsx index eb82b9d..574d37f 100644 --- a/src/components/RSVP.tsx +++ b/src/components/RSVP.tsx @@ -63,13 +63,12 @@ const RSVPForm = () => { dietaryRestrictions: "", }); setSuccess(true); - } catch (err) { - if (err.name === 'AbortError') { + } catch (err: any) { + if (err?.name === 'AbortError') { setError('Request timed out. Please try again.'); } else { - setError( - err instanceof Error ? err.message : "Failed to submit RSVP. Please try again." - ); + const errorMessage = (err instanceof Error && err.message) ? err.message : "Failed to submit RSVP. Please try again."; + setError(errorMessage); } console.error("Error submitting RSVP:", err); } finally { diff --git a/src/components/RegistryList.tsx b/src/components/RegistryList.tsx new file mode 100644 index 0000000..05a12bb --- /dev/null +++ b/src/components/RegistryList.tsx @@ -0,0 +1,152 @@ +import { useState, useEffect } from "react"; +import type { RegistryItem } from "../lib/types"; + +const RegistryList = () => { + const [registryItems, setRegistryItems] = useState([]); + const [claimedItems, setClaimedItems] = useState>(new Set()); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchRegistryItems = async () => { + setLoading(true); + setError(null); + try { + const response = await fetch("/api/registry"); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + setRegistryItems(data); + } catch (e: any) { + setError(e.message); + console.error("Failed to fetch registry items:", e); + } finally { + setLoading(false); + } + }; + + fetchRegistryItems(); + }, []); + + const handleCheckboxChange = (itemId: string) => { + const newClaimedItems = new Set(claimedItems); + if (newClaimedItems.has(itemId)) { + newClaimedItems.delete(itemId); + } else { + newClaimedItems.add(itemId); + } + setClaimedItems(newClaimedItems); + }; + + const handleSubmit = async () => { + // Prepare updates for claimed items + const updates = registryItems.filter((item) => + claimedItems.has(item.id) + ); + + // Optimistically update the UI + const updatedRegistryItems = registryItems.map((item) => { + if (claimedItems.has(item.id)) { + return { ...item, taken: true }; + } + return item; + }); + setRegistryItems(updatedRegistryItems); + setClaimedItems(new Set()); // Clear claimed items after submission + + try { + // Send updates to the server + for (const item of updates) { + const response = await fetch(`/api/registry/${item.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ taken: true }), + }); + + if (!response.ok) { + throw new Error( + `Failed to update item ${item.id}: ${response.statusText}` + ); + } + } + alert("Thank you for claiming items!"); + } catch (error: any) { + console.error("Error updating items:", error); + setError("Failed to update items. Please try again."); + setRegistryItems(registryItems); + } + }; + + if (loading) { + return
Loading registry items...
; + } + + if (error) { + return ( +
Error: {error}
+ ); + } + + return ( +
+

Baby Registry

+
+ + + + + + + + + + {registryItems.map((item) => ( + + + + + + ))} + +
ItemLinkClaim
+ {item.name} + {item.taken && ( + Taken + )} + + {item.link && ( + + View + + )} + + {!item.taken && ( + handleCheckboxChange(item.id)} + /> + )} +
+
+ +
+ ); +}; + +export default RegistryList; diff --git a/src/components/RegistryManager.tsx b/src/components/RegistryManager.tsx new file mode 100644 index 0000000..7af6e8d --- /dev/null +++ b/src/components/RegistryManager.tsx @@ -0,0 +1,184 @@ +import { useState, useEffect } from "react"; +import type { RegistryItem } from "../lib/types"; + +const RegistryManager = () => { + const [registryItems, setRegistryItems] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [newItem, setNewItem] = useState({ name: "", link: "" }); + + useEffect(() => { + fetchRegistryItems(); + }, []); + + const fetchRegistryItems = async () => { + setLoading(true); + setError(null); + try { + const response = await fetch("/api/registry"); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + setRegistryItems(data); + } catch (e: any) { + setError(e.message); + console.error("Failed to fetch registry items:", e); + } finally { + setLoading(false); + } + }; + + const handleAddItem = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newItem.name.trim()) return; + + try { + const response = await fetch("/api/registry", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(newItem), + }); + + if (!response.ok) { + throw new Error(`Failed to add item: ${response.statusText}`); + } + + // Clear form and refresh list + setNewItem({ name: "", link: "" }); + await fetchRegistryItems(); + } catch (error: any) { + setError(error.message); + console.error("Error adding item:", error); + } + }; + + const handleDeleteItem = async (itemId: string) => { + if (!confirm("Are you sure you want to delete this item?")) return; + + try { + const response = await fetch(`/api/registry/${itemId}`, { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error(`Failed to delete item: ${response.statusText}`); + } + + // Refresh the list after deletion + await fetchRegistryItems(); + } catch (error: any) { + setError(error.message); + console.error("Error deleting item:", error); + } + }; + + if (loading) { + return
Loading registry items...
; + } + + if (error) { + return
Error: {error}
; + } + + return ( +
+

Registry Manager

+ + {/* Add New Item Form */} +
+
+
+

Add New Item

+
+ + + setNewItem({ ...newItem, name: e.target.value }) + } + required + /> +
+
+ + + setNewItem({ ...newItem, link: e.target.value }) + } + /> +
+
+ +
+
+
+
+ + {/* Registry Items List */} +
+ + + + + + + + + + + {registryItems.map((item) => ( + + + + + + + ))} + +
ItemLinkStatusActions
{item.name} + {item.link && ( + + View + + )} + + {item.taken ? ( + Taken + ) : ( + Available + )} + + +
+
+
+ ); +}; + +export default RegistryManager; diff --git a/src/components/SignIn.tsx b/src/components/SignIn.tsx index 4938910..a294ab5 100644 --- a/src/components/SignIn.tsx +++ b/src/components/SignIn.tsx @@ -2,9 +2,16 @@ import React, { useState } from "react"; interface SignInProps { onSuccess?: () => void; + requiredRole?: "guest" | "admin"; } -const SignIn = ({ onSuccess }: SignInProps) => { +interface ApiResponse { + success: boolean; + error?: string; + role?: string; +} + +const SignIn = ({ onSuccess, requiredRole = "guest" }: SignInProps) => { const [code, setCode] = useState(""); const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); @@ -15,32 +22,37 @@ const SignIn = ({ onSuccess }: SignInProps) => { setIsSubmitting(true); try { + // Clear existing authentication before attempting new sign-in + sessionStorage.removeItem("isAuthenticated"); + sessionStorage.removeItem("role"); + const response = await fetch("/api/auth", { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ code }), + body: JSON.stringify({ code, requiredRole }), }); - const data = await response.json(); + const data: ApiResponse = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || "Invalid code"); } sessionStorage.setItem("isAuthenticated", "true"); - - // Dispatch custom event instead of calling callback - const event = new CustomEvent('auth-success', { + sessionStorage.setItem("role", data.role || "guest"); + + // Dispatch a custom event with the new role + const event = new CustomEvent("auth-success", { bubbles: true, - composed: true + composed: true, + detail: { role: data.role } }); document.dispatchEvent(event); - - // Still call onSuccess if provided (for flexibility) + onSuccess?.(); - } catch (err) { + } catch (err: any) { setError(err instanceof Error ? err.message : "Authentication failed"); } finally { setIsSubmitting(false); diff --git a/src/components/SignOut.tsx b/src/components/SignOut.tsx new file mode 100644 index 0000000..a5d0baa --- /dev/null +++ b/src/components/SignOut.tsx @@ -0,0 +1,33 @@ +import React, { useEffect, useState } from "react"; + +const SignOut = () => { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const isAuthenticated = sessionStorage.getItem("isAuthenticated") === "true"; + setIsVisible(isAuthenticated); + + const handleAuthChange = () => { + setIsVisible(sessionStorage.getItem("isAuthenticated") === "true"); + }; + + document.addEventListener("auth-success", handleAuthChange); + return () => document.removeEventListener("auth-success", handleAuthChange); + }, []); + + const handleSignOut = () => { + sessionStorage.removeItem("isAuthenticated"); + sessionStorage.removeItem("role"); + window.location.reload(); + }; + + if (!isVisible) return null; + + return ( + + ); +}; + +export default SignOut; diff --git a/src/lib/s3.ts b/src/lib/s3.ts new file mode 100644 index 0000000..67bc6b3 --- /dev/null +++ b/src/lib/s3.ts @@ -0,0 +1,66 @@ +import { + S3Client, + GetObjectCommand, + PutObjectCommand, +} from "@aws-sdk/client-s3"; + +const s3Host = process.env.S3_HOST || import.meta.env.S3_HOST; +const endpoint = `https://${s3Host}`; + +const s3Client = new S3Client({ + region: process.env.S3_REGION || import.meta.env.S3_REGION, + endpoint, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY || import.meta.env.S3_ACCESS_KEY, + secretAccessKey: + process.env.S3_SECRET_KEY || import.meta.env.S3_SECRET_KEY, + }, + forcePathStyle: true, +}); + +const BUCKET_NAME = + process.env.S3_BUCKET_NAME || import.meta.env.S3_BUCKET_NAME; + +export async function getS3Data(key: string): Promise { + try { + const getCommand = new GetObjectCommand({ + Bucket: BUCKET_NAME, + Key: key, + }); + + const response = await s3Client.send(getCommand); + const fileContent = await response.Body?.transformToString(); + + if (fileContent) { + return JSON.parse(fileContent) as T; + } else { + console.warn(`File ${key} is empty.`); + return undefined; + } + } catch (error: any) { + console.warn(`Error getting data from S3 at key ${key}:`, error.message); + return undefined; + } +} + +export async function putS3Data( + key: string, + data: T, + contentType: string = "application/json" +): Promise { + try { + const body = JSON.stringify(data, null, 2); // Pretty-print JSON + const putCommand = new PutObjectCommand({ + Bucket: BUCKET_NAME, + Key: key, + Body: body, + ContentType: contentType, + }); + + await s3Client.send(putCommand); + console.log(`Successfully saved data to S3 at key: ${key}`); + } catch (error: any) { + console.error(`Error putting data to S3 at key ${key}:`, error.message); + throw error; // Re-throw to handle in the API route + } +} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..9c20706 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,12 @@ +export interface RegistryItem { + id: string; + name: string; + taken: boolean; + link?: string; +} + +export interface RSVPItem { + name: string; + dietaryRestrictions: string; + timestamp: string; +} \ No newline at end of file diff --git a/src/pages/api/auth.ts b/src/pages/api/auth.ts index 0cfd7c6..b84131e 100644 --- a/src/pages/api/auth.ts +++ b/src/pages/api/auth.ts @@ -1,18 +1,25 @@ import type { APIRoute } from "astro"; +interface AuthRequest { + code: string; + requiredRole?: "guest" | "admin"; +} + export const POST: APIRoute = async ({ request }) => { const headers = { "Content-Type": "application/json", }; try { - const { code } = await request.json(); + const { code, requiredRole = "guest" } = (await request.json()) as AuthRequest; const secretCode = process.env.SECRET_CODE || import.meta.env.SECRET_CODE; + const adminCode = process.env.ADMIN_CODE || import.meta.env.ADMIN_CODE; - console.log("Received code:", code); // For debugging - console.log("Secret code:", secretCode); // For debugging - - if (!code || code !== secretCode) { + let role = "guest"; + + if (code === adminCode) { + role = "admin"; + } else if (code !== secretCode) { return new Response( JSON.stringify({ success: false, @@ -25,11 +32,25 @@ export const POST: APIRoute = async ({ request }) => { ); } - return new Response(JSON.stringify({ success: true }), { + if (requiredRole === "admin" && role !== "admin") { + return new Response( + JSON.stringify({ + success: false, + error: "Admin access required", + }), + { + status: 403, + headers, + } + ); + } + + return new Response(JSON.stringify({ success: true, role }), { status: 200, headers, }); } catch (error) { + console.error("Authentication failed:", error); return new Response( JSON.stringify({ success: false, diff --git a/src/pages/api/registry/[id].ts b/src/pages/api/registry/[id].ts new file mode 100644 index 0000000..3d9b4a2 --- /dev/null +++ b/src/pages/api/registry/[id].ts @@ -0,0 +1,123 @@ +import type { APIRoute } from "astro"; +import { getS3Data, putS3Data } from "../../../lib/s3"; +import type { RegistryItem } from "../../../lib/types"; + +const REGISTRY_FILE_KEY = "baby-registry.json"; + +// GET: Get a specific registry item by ID +export const GET: APIRoute = async ({ params }) => { + try { + const { id } = params; + if (!id) { + return new Response( + JSON.stringify({ success: false, error: "Item ID is required" }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + const registry = await getS3Data(REGISTRY_FILE_KEY) || []; + const item = registry.find((item) => item.id === id); + + if (!item) { + return new Response( + JSON.stringify({ success: false, error: "Item not found" }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + return new Response(JSON.stringify(item), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error: any) { + console.error("Error getting registry item:", error.message); + return new Response( + JSON.stringify({ success: false, error: "Failed to get item" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}; + +// PUT: Update an existing registry item +export const PUT: APIRoute = async ({ request, params }) => { + try { + const { id } = params; + if (!id) { + return new Response( + JSON.stringify({ success: false, error: "Item ID is required" }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + const body = await request.json(); + const { name, taken, link } = body; + + const registry = await getS3Data(REGISTRY_FILE_KEY) || []; + const itemIndex = registry.findIndex((item) => item.id === id); + + if (itemIndex === -1) { + return new Response( + JSON.stringify({ success: false, error: "Item not found" }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + // Update the item with the provided values + registry[itemIndex] = { + ...registry[itemIndex], + name: name !== undefined ? name : registry[itemIndex].name, + taken: taken !== undefined ? taken : registry[itemIndex].taken, + link: link !== undefined ? link : registry[itemIndex].link, + }; + + await putS3Data(REGISTRY_FILE_KEY, registry); + + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error: any) { + console.error("Error updating registry item:", error.message); + return new Response( + JSON.stringify({ success: false, error: "Failed to update item" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}; + +// DELETE: Delete a registry item +export const DELETE: APIRoute = async ({ params }) => { + try { + const { id } = params; + if (!id) { + return new Response( + JSON.stringify({ success: false, error: "Item ID is required" }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + const registry = await getS3Data(REGISTRY_FILE_KEY) || []; + const itemIndex = registry.findIndex((item) => item.id === id); + + if (itemIndex === -1) { + return new Response( + JSON.stringify({ success: false, error: "Item not found" }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + registry.splice(itemIndex, 1); // Remove the item from the array + await putS3Data(REGISTRY_FILE_KEY, registry); + + return new Response(JSON.stringify({ success: true }), { + status: 204, // No Content + headers: { "Content-Type": "application/json" }, + }); + } catch (error: any) { + console.error("Error deleting registry item:", error.message); + return new Response( + JSON.stringify({ success: false, error: "Failed to delete item" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}; diff --git a/src/pages/api/registry/index.ts b/src/pages/api/registry/index.ts new file mode 100644 index 0000000..4dbcfcd --- /dev/null +++ b/src/pages/api/registry/index.ts @@ -0,0 +1,60 @@ +import type { APIRoute } from "astro"; +import { getS3Data, putS3Data } from "../../../lib/s3"; +import { v4 as uuidv4 } from "uuid"; +import type { RegistryItem } from "../../../lib/types"; + +const REGISTRY_FILE_KEY = "baby-registry.json"; + +// GET: List all registry items +export const GET: APIRoute = async () => { + try { + const registry = await getS3Data(REGISTRY_FILE_KEY) || []; + return new Response(JSON.stringify(registry), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error: any) { + console.error("Error listing registry items:", error.message); + return new Response( + JSON.stringify({ success: false, error: "Failed to list items" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}; + +// POST: Create a new registry item +export const POST: APIRoute = async ({ request }) => { + try { + const body = await request.json(); + const { name, link } = body; + + if (!name) { + return new Response( + JSON.stringify({ success: false, error: "Name is required" }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + const registry = await getS3Data(REGISTRY_FILE_KEY) || []; + const newItem: RegistryItem = { + id: uuidv4(), + name, + taken: false, + link, + }; + + registry.push(newItem); + await putS3Data(REGISTRY_FILE_KEY, registry); + + return new Response( + JSON.stringify({ success: true, itemId: newItem.id }), + { status: 201, headers: { "Content-Type": "application/json" } } + ); + } catch (error: any) { + console.error("Error adding item to registry:", error.message); + return new Response( + JSON.stringify({ success: false, error: "Failed to add item" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}; diff --git a/src/pages/api/rsvp.ts b/src/pages/api/rsvp.ts index 291feba..53960c4 100644 --- a/src/pages/api/rsvp.ts +++ b/src/pages/api/rsvp.ts @@ -1,24 +1,14 @@ -import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; import type { APIRoute } from "astro"; +import { getS3Data, putS3Data } from "../../lib/s3"; +import type { RSVPItem } from "../../lib/types"; -// Define the structure of our RSVP data -interface RSVPEntry { - name: string; - dietaryRestrictions: string; - timestamp: string; -} - -// Helper function to convert array of objects to CSV string -const objectsToCSV = (data: RSVPEntry[]): string => { - // Define headers +const objectsToCSV = (data: RSVPItem[]): string => { const headers = ["name", "dietaryRestrictions", "timestamp"]; const csvRows = [headers.join(",")]; - // Add data rows data.forEach((entry) => { const row = headers.map((header) => { - // Escape commas and quotes in the content - const field = String(entry[header as keyof RSVPEntry]); + const field = String(entry[header as keyof RSVPItem]); const escaped = field.replace(/"/g, '""'); return `"${escaped}"`; }); @@ -28,26 +18,24 @@ const objectsToCSV = (data: RSVPEntry[]): string => { return csvRows.join("\n"); }; -// Helper function to parse CSV string to array of objects -const csvToObjects = (csv: string): RSVPEntry[] => { +const csvToObjects = (csv: string): RSVPItem[] => { const lines = csv.split("\n"); - const headers = lines[0].split(",").map(h => h.trim()); - + const headers = lines[0].split(",").map((h) => h.trim()); + return lines - .slice(1) // Skip headers - .filter(line => line.trim()) // Skip empty lines - .map(line => { + .slice(1) + .filter((line) => line.trim()) + .map((line) => { const values = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || []; - const entry: Partial = {}; - + const entry: Partial = {}; + headers.forEach((header, index) => { let value = values[index] || ""; - // Remove quotes and unescape doubled quotes value = value.replace(/^"(.*)"$/, "$1").replace(/""/g, '"'); - entry[header as keyof RSVPEntry] = value; + entry[header as keyof RSVPItem] = value; }); - return entry as RSVPEntry; + return entry as RSVPItem; }); }; @@ -60,54 +48,21 @@ export const POST: APIRoute = async ({ request }) => { }; try { - const s3Host = process.env.S3_HOST || import.meta.env.S3_HOST; - const endpoint = `https://${s3Host}`; + const FILE_KEY = "rsvp.csv"; - console.log("Creating S3 client with config:", { - region: process.env.S3_REGION || import.meta.env.S3_REGION, - endpoint, - }); - - const s3Client = new S3Client({ - region: process.env.S3_REGION || import.meta.env.S3_REGION, - endpoint, - credentials: { - accessKeyId: process.env.S3_ACCESS_KEY || import.meta.env.S3_ACCESS_KEY, - secretAccessKey: process.env.S3_SECRET_KEY || import.meta.env.S3_SECRET_KEY, - }, - forcePathStyle: true, - }); - - const BUCKET_NAME = process.env.S3_BUCKET_NAME || import.meta.env.S3_BUCKET_NAME; - const FILE_KEY = "rsvp.csv"; // Changed to CSV extension - console.log("Parsing request body"); const newRsvp = await request.json(); console.log("Received RSVP data:", newRsvp); - // Try to get existing RSVPs - let existingRsvps: RSVPEntry[] = []; - try { - console.log("Attempting to fetch existing RSVPs"); - const getCommand = new GetObjectCommand({ - Bucket: BUCKET_NAME, - Key: FILE_KEY, - }); - - console.log("Sending GET request to S3"); - const response = await s3Client.send(getCommand); - console.log("Received response from S3 GET"); - - const fileContent = await response.Body?.transformToString(); - if (fileContent) { - existingRsvps = csvToObjects(fileContent); - console.log("Existing RSVPs loaded:", existingRsvps.length); - } - } catch (error) { - console.log("No existing RSVP file found or error:", error); + 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); } - // Add new RSVP existingRsvps.push({ ...newRsvp, timestamp: new Date().toISOString(), @@ -115,17 +70,8 @@ export const POST: APIRoute = async ({ request }) => { console.log("Attempting to save updated RSVP list"); const csvContent = objectsToCSV(existingRsvps); - - const putCommand = new PutObjectCommand({ - Bucket: BUCKET_NAME, - Key: FILE_KEY, - Body: csvContent, - ContentType: "text/csv", - }); - console.log("Sending PUT request to S3"); - await s3Client.send(putCommand); - console.log("Successfully saved to S3"); + await putS3Data(FILE_KEY, csvContent, "text/csv"); return new Response(JSON.stringify({ success: true }), { status: 200, diff --git a/src/pages/index.astro b/src/pages/index.astro index 45d3c29..e6072d4 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,5 +1,6 @@ --- import Layout from "../layouts/Layout.astro"; +import SignOut from "../components/SignOut.tsx"; --- @@ -7,7 +8,9 @@ import Layout from "../layouts/Layout.astro";
❤️ Natasha + Ixabat ❤️
diff --git a/src/pages/registry/admin.astro b/src/pages/registry/admin.astro new file mode 100644 index 0000000..e8d55dc --- /dev/null +++ b/src/pages/registry/admin.astro @@ -0,0 +1,55 @@ +--- +import Layout from "../../layouts/Layout.astro"; +import RegistryManager from "../../components/RegistryManager.tsx"; +import SignIn from "../../components/SignIn.tsx"; +import SignOut from "../../components/SignOut.tsx"; +--- + + +
+
Registry Manager
+ +
+ {}} requiredRole="admin" /> +
+ + + +
+ Home + +
+
+
+ + diff --git a/src/pages/registry/index.astro b/src/pages/registry/index.astro new file mode 100644 index 0000000..55a37c8 --- /dev/null +++ b/src/pages/registry/index.astro @@ -0,0 +1,47 @@ +--- +import Layout from "../../layouts/Layout.astro"; +import RegistryList from "../../components/RegistryList.tsx"; +import SignIn from "../../components/SignIn.tsx"; +import SignOut from "../../components/SignOut.tsx"; +--- + + +
+
+ View and Claim Items from the Baby Registry: +
+ +
+ {}} requiredRole="guest" /> +
+ + + +
+ Home + +
+
+
+ + diff --git a/src/pages/rsvp.astro b/src/pages/rsvp.astro index 33d671a..0f18190 100644 --- a/src/pages/rsvp.astro +++ b/src/pages/rsvp.astro @@ -2,6 +2,7 @@ import Layout from "../layouts/Layout.astro"; import RSVP from "../components/RSVP.tsx"; import SignIn from "../components/SignIn.tsx"; +import SignOut from "../components/SignOut.tsx"; --- @@ -11,7 +12,7 @@ import SignIn from "../components/SignIn.tsx";
- {}} /> + {}} requiredRole="guest" />