Changes
This commit is contained in:
BIN
public/hero.jpeg
BIN
public/hero.jpeg
Binary file not shown.
Before Width: | Height: | Size: 473 KiB |
BIN
public/logo.jpg
Normal file
BIN
public/logo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 387 KiB |
@ -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"
|
||||
)}
|
||||
|
@ -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 (
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="stats shadow mb-8 w-full">
|
||||
<div className="flex flex-col gap-4 mx-auto p-4">
|
||||
<div className="stats shadow mb-8 mx-auto">
|
||||
<div className="stat">
|
||||
<div className="stat-title">Total RSVPs</div>
|
||||
<div className="stat-title">Total Responses</div>
|
||||
<div className="stat-value">{rsvpList.length}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-title">Attending</div>
|
||||
<div className="stat-value text-success">{attending.length}</div>
|
||||
<div className="stat-desc">Will be there</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-title">Not Attending</div>
|
||||
<div className="stat-value text-error">{notAttending.length}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-title">No Response</div>
|
||||
<div className="stat-value text-warning">{noResponse.length}</div>
|
||||
<div className="stat-desc">Can't make it</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto mx-auto">
|
||||
<table className="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -88,7 +110,8 @@ const RSVPManager = () => {
|
||||
<th>Status</th>
|
||||
<th>Dietary Restrictions</th>
|
||||
<th>Notes</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Response Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -96,19 +119,29 @@ const RSVPManager = () => {
|
||||
<tr key={index}>
|
||||
<td>{rsvp.name}</td>
|
||||
<td>
|
||||
<div className={`badge ${
|
||||
rsvp.attending === true ? 'badge-success' :
|
||||
rsvp.attending === false ? 'badge-error' :
|
||||
'badge-warning'
|
||||
}`}>
|
||||
{rsvp.attending === true ? 'Attending' :
|
||||
rsvp.attending === false ? 'Not Attending' :
|
||||
'No Response'}
|
||||
</div>
|
||||
<span className={`badge badge-lg ${
|
||||
rsvp.attending ? 'badge-success' : 'badge-error'
|
||||
} whitespace-nowrap`}>
|
||||
{rsvp.attending ? 'Attending' : 'Not Attending'}
|
||||
</span>
|
||||
</td>
|
||||
<td>{rsvp.dietaryRestrictions && rsvp.dietaryRestrictions !== "undefined" ? rsvp.dietaryRestrictions : "-"}</td>
|
||||
<td>{rsvp.notes && rsvp.notes !== "undefined" ? rsvp.notes : "-"}</td>
|
||||
<td className="whitespace-nowrap">{new Date(rsvp.timestamp).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={() => handleDeleteRSVP(rsvp.name, rsvp.timestamp)}
|
||||
className="btn btn-error btn-sm"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
<td>{rsvp.attending ? (rsvp.dietaryRestrictions || "None") : "-"}</td>
|
||||
<td>{(rsvp.notes && rsvp.notes !== "undefined") ? rsvp.notes.trim() : "None"}</td>
|
||||
<td>{new Date(rsvp.timestamp).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
@ -4,10 +4,11 @@ import { fetchWithAuth, getAuthToken } from "../utils/auth-client";
|
||||
|
||||
const RegistryList = () => {
|
||||
const [registryItems, setRegistryItems] = useState<RegistryItem[]>([]);
|
||||
const [claimedItems, setClaimedItems] = useState<Set<string>>(new Set());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [claimantName, setClaimantName] = useState("");
|
||||
const [claimedItems, setClaimedItems] = useState<Set<string>>(new Set());
|
||||
const [showClaimants, setShowClaimants] = useState<Set<string>>(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 <div className="text-center">Initializing...</div>;
|
||||
}
|
||||
return <div className="text-red-500 text-center">Error: {error}</div>;
|
||||
}
|
||||
|
||||
const availableItems = registryItems.filter((item) => !item.taken);
|
||||
const claimedItemsArray = registryItems.filter((item) => item.taken);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
{availableItems.length > 0 && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={claimantName}
|
||||
onChange={(e) => setClaimantName(e.target.value)}
|
||||
placeholder="Enter your name"
|
||||
className="input input-bordered w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
|
||||
{availableItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`card bg-base-100 shadow-xl cursor-pointer ${
|
||||
claimedItems.has(item.id)
|
||||
? "border-4 border-primary"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => handleItemClick(item.id)}
|
||||
>
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">{item.name}</h2>
|
||||
<h1 className="text-2xl font-bold mb-4">Wedding Registry</h1>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Link</th>
|
||||
<th>Status</th>
|
||||
<th>Claim</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{registryItems.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{item.name}
|
||||
{item.taken && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="badge badge-success">Taken</span>
|
||||
<button
|
||||
onClick={() => toggleClaimantVisibility(item.id)}
|
||||
className="btn btn-xs btn-ghost"
|
||||
>
|
||||
{showClaimants.has(item.id) ? "Hide" : "Show"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{item.taken &&
|
||||
item.claimedBy &&
|
||||
showClaimants.has(item.id) && (
|
||||
<div className="text-sm text-gray-500 italic">
|
||||
Claimed by: {item.claimedBy}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{item.link && (
|
||||
<a
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link link-primary"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
View Item
|
||||
View
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{claimedItems.size > 0 && (
|
||||
<div className="text-center mb-8">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="btn btn-primary"
|
||||
disabled={!claimantName.trim()}
|
||||
>
|
||||
Claim Selected Items
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{claimedItemsArray.length > 0 && (
|
||||
<div className="mt-8">
|
||||
<h2 className="text-2xl font-bold mb-4">Already Claimed</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{claimedItemsArray.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="card bg-base-100 shadow-xl opacity-50"
|
||||
>
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">{item.name}</h2>
|
||||
<p className="text-sm">
|
||||
Claimed by: {item.claimedBy}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{item.taken ? "Claimed" : "Available"}</td>
|
||||
<td>
|
||||
{!item.taken ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
checked={claimedItems.has(item.id)}
|
||||
onChange={() => handleCheckboxChange(item.id)}
|
||||
/>
|
||||
) : <input
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
disabled
|
||||
checked={true}
|
||||
/>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{claimedItems.size > 0 && (
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Your Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input input-bordered"
|
||||
value={claimantName}
|
||||
onChange={(e) => setClaimantName(e.target.value)}
|
||||
placeholder="Enter your name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={!claimantName.trim()}
|
||||
>
|
||||
Submit Claims
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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 (
|
||||
<div className="container mx-auto p-4">
|
||||
<form onSubmit={handleAddItem} className="mb-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={newItem.name}
|
||||
onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
|
||||
placeholder="Item name"
|
||||
className="input input-bordered"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="url"
|
||||
value={newItem.link}
|
||||
onChange={(e) => setNewItem({ ...newItem, link: e.target.value })}
|
||||
placeholder="Item link (optional)"
|
||||
className="input input-bordered"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Add Item
|
||||
</button>
|
||||
<form onSubmit={handleAddItem} className="mb-8 max-w-2xl mx-auto">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="form-control w-full">
|
||||
<input
|
||||
type="text"
|
||||
value={newItem.name}
|
||||
onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
|
||||
placeholder="Item name"
|
||||
className="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control w-full">
|
||||
<input
|
||||
type="url"
|
||||
value={newItem.link}
|
||||
onChange={(e) => setNewItem({ ...newItem, link: e.target.value })}
|
||||
placeholder="Item link (optional)"
|
||||
className="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control w-full">
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
Add Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -185,12 +212,22 @@ const RegistryManager = () => {
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={() => handleDeleteItem(item.id)}
|
||||
className="btn btn-error btn-sm"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
{item.taken && (
|
||||
<button
|
||||
onClick={() => handleReleaseClaim(item.id)}
|
||||
className="btn btn-warning btn-sm"
|
||||
>
|
||||
Release Claim
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleDeleteItem(item.id)}
|
||||
className="btn btn-error btn-sm"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
@ -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<RSVPItem> = {
|
||||
name: "",
|
||||
dietaryRestrictions: "",
|
||||
notes: "",
|
||||
timestamp: "",
|
||||
};
|
||||
const entry: Partial<RSVPItem> = {};
|
||||
|
||||
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<string>(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<string>(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");
|
||||
|
@ -13,7 +13,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Dress Code?
|
||||
Dress Code
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p class="text-lg">
|
||||
@ -27,7 +27,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Dietary Restrictions?
|
||||
Dietary Restrictions
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p class="text-lg">
|
||||
@ -41,7 +41,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Alcohol?
|
||||
Alcohol
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p class="text-lg">
|
||||
@ -54,7 +54,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Childcare?
|
||||
Childcare
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p class="text-lg">
|
||||
@ -68,7 +68,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Gifts?
|
||||
Gifts
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p class="text-lg">
|
||||
@ -99,7 +99,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Where Do I Contact You With All of My Concerns?
|
||||
I have all these concerns, what do I do?
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<div class="text-lg space-y-2">
|
||||
|
@ -10,10 +10,10 @@ import SignIn from "../components/SignIn.tsx";
|
||||
|
||||
<div id="content-container" class="hidden">
|
||||
<div class="flex flex-col gap-8 max-w-5xl mx-auto p-6">
|
||||
<img src="/hero.jpeg" alt="" height="250" width="250" class="rounded-full mx-auto">
|
||||
<img src="/logo.jpg" alt="" height="250" width="250" class="rounded-full mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="text-center space-y-4">
|
||||
<div class="text-5xl font-normal" style="font-family: 'Great Vibes', cursive;">❤️ Natasha + Ixabat ❤️</div>
|
||||
<div class="text-5xl font-normal" style="font-family: 'Great Vibes', cursive;"><span class="text-2xl">❤️</span> Natasha + Ixabat <span class="text-3xl">❤️</span></div>
|
||||
<p class="text-xl text-gray-600">We hope you can join us in celebration on</p>
|
||||
<p class="text-2xl font-semibold">Saturday, June 7, 2025</p>
|
||||
</div>
|
||||
@ -21,7 +21,7 @@ import SignIn from "../components/SignIn.tsx";
|
||||
<!-- Event Details Cards -->
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<!-- Ceremony Card -->
|
||||
<div class="card bg-base-100 shadow-xl flex-1">
|
||||
<div class="card bg-base-100 shadow-xl flex-1 border-2 border-primary">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl justify-center">Ceremony</h2>
|
||||
<div class="text-center space-y-2">
|
||||
@ -40,7 +40,7 @@ import SignIn from "../components/SignIn.tsx";
|
||||
</div>
|
||||
|
||||
<!-- Reception Card -->
|
||||
<div class="card bg-base-100 shadow-xl flex-1">
|
||||
<div class="card bg-base-100 shadow-xl flex-1 border-2 border-primary">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl justify-center">Reception</h2>
|
||||
<div class="text-center space-y-2">
|
||||
|
Reference in New Issue
Block a user