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: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"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,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -242,7 +246,7 @@ const RSVPForm = () => {
|
|||||||
) : formData.attending ? (
|
) : formData.attending ? (
|
||||||
"Yes, I'll Be There!"
|
"Yes, I'll Be There!"
|
||||||
) : formData.attending === false ? (
|
) : formData.attending === false ? (
|
||||||
"Send My Regrets"
|
"Submit RSVP"
|
||||||
) : (
|
) : (
|
||||||
"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(() => {
|
useEffect(() => {
|
||||||
fetchRSVPList();
|
fetchRSVPList();
|
||||||
|
|
||||||
@ -57,30 +82,27 @@ const RSVPManager = () => {
|
|||||||
|
|
||||||
const attending = rsvpList.filter(rsvp => rsvp.attending === true);
|
const attending = rsvpList.filter(rsvp => rsvp.attending === true);
|
||||||
const notAttending = rsvpList.filter(rsvp => rsvp.attending === false);
|
const notAttending = rsvpList.filter(rsvp => rsvp.attending === false);
|
||||||
const noResponse = rsvpList.filter(rsvp => rsvp.attending === undefined || rsvp.attending === null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<div className="flex flex-col gap-4 mx-auto p-4">
|
||||||
<div className="stats shadow mb-8 w-full">
|
<div className="stats shadow mb-8 mx-auto">
|
||||||
<div className="stat">
|
<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 className="stat-value">{rsvpList.length}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<div className="stat-title">Attending</div>
|
<div className="stat-title">Attending</div>
|
||||||
<div className="stat-value text-success">{attending.length}</div>
|
<div className="stat-value text-success">{attending.length}</div>
|
||||||
|
<div className="stat-desc">Will be there</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<div className="stat-title">Not Attending</div>
|
<div className="stat-title">Not Attending</div>
|
||||||
<div className="stat-value text-error">{notAttending.length}</div>
|
<div className="stat-value text-error">{notAttending.length}</div>
|
||||||
</div>
|
<div className="stat-desc">Can't make it</div>
|
||||||
<div className="stat">
|
|
||||||
<div className="stat-title">No Response</div>
|
|
||||||
<div className="stat-value text-warning">{noResponse.length}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto mx-auto">
|
||||||
<table className="table w-full">
|
<table className="table w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -88,7 +110,8 @@ const RSVPManager = () => {
|
|||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Dietary Restrictions</th>
|
<th>Dietary Restrictions</th>
|
||||||
<th>Notes</th>
|
<th>Notes</th>
|
||||||
<th>Timestamp</th>
|
<th>Response Date</th>
|
||||||
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -96,19 +119,29 @@ const RSVPManager = () => {
|
|||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>{rsvp.name}</td>
|
<td>{rsvp.name}</td>
|
||||||
<td>
|
<td>
|
||||||
<div className={`badge ${
|
<span className={`badge badge-lg ${
|
||||||
rsvp.attending === true ? 'badge-success' :
|
rsvp.attending ? 'badge-success' : 'badge-error'
|
||||||
rsvp.attending === false ? 'badge-error' :
|
} whitespace-nowrap`}>
|
||||||
'badge-warning'
|
{rsvp.attending ? 'Attending' : 'Not Attending'}
|
||||||
}`}>
|
</span>
|
||||||
{rsvp.attending === true ? 'Attending' :
|
</td>
|
||||||
rsvp.attending === false ? 'Not Attending' :
|
<td>{rsvp.dietaryRestrictions && rsvp.dietaryRestrictions !== "undefined" ? rsvp.dietaryRestrictions : "-"}</td>
|
||||||
'No Response'}
|
<td>{rsvp.notes && rsvp.notes !== "undefined" ? rsvp.notes : "-"}</td>
|
||||||
</div>
|
<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>
|
||||||
<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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -4,10 +4,11 @@ import { fetchWithAuth, getAuthToken } from "../utils/auth-client";
|
|||||||
|
|
||||||
const RegistryList = () => {
|
const RegistryList = () => {
|
||||||
const [registryItems, setRegistryItems] = useState<RegistryItem[]>([]);
|
const [registryItems, setRegistryItems] = useState<RegistryItem[]>([]);
|
||||||
|
const [claimedItems, setClaimedItems] = useState<Set<string>>(new Set());
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [claimantName, setClaimantName] = useState("");
|
const [claimantName, setClaimantName] = useState("");
|
||||||
const [claimedItems, setClaimedItems] = useState<Set<string>>(new Set());
|
const [showClaimants, setShowClaimants] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const fetchRegistryItems = async () => {
|
const fetchRegistryItems = async () => {
|
||||||
// Don't try to fetch if we don't have a token yet
|
// 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);
|
return () => document.removeEventListener("auth-success", handleAuthSuccess);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleItemClick = (itemId: string) => {
|
const handleCheckboxChange = (itemId: string) => {
|
||||||
const newClaimedItems = new Set(claimedItems);
|
const newClaimedItems = new Set(claimedItems);
|
||||||
if (newClaimedItems.has(itemId)) {
|
if (newClaimedItems.has(itemId)) {
|
||||||
newClaimedItems.delete(itemId);
|
newClaimedItems.delete(itemId);
|
||||||
@ -62,6 +63,16 @@ const RegistryList = () => {
|
|||||||
setClaimedItems(newClaimedItems);
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
if (!claimantName.trim()) {
|
if (!claimantName.trim()) {
|
||||||
setError("Please enter your name before claiming items");
|
setError("Please enter your name before claiming items");
|
||||||
@ -109,87 +120,108 @@ const RegistryList = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
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>;
|
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 (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4">
|
||||||
{availableItems.length > 0 && (
|
<h1 className="text-2xl font-bold mb-4">Wedding Registry</h1>
|
||||||
<>
|
<div className="overflow-x-auto">
|
||||||
<div className="mb-4">
|
<table className="table w-full">
|
||||||
<input
|
<thead>
|
||||||
type="text"
|
<tr>
|
||||||
value={claimantName}
|
<th>Item</th>
|
||||||
onChange={(e) => setClaimantName(e.target.value)}
|
<th>Link</th>
|
||||||
placeholder="Enter your name"
|
<th>Status</th>
|
||||||
className="input input-bordered w-full max-w-xs"
|
<th>Claim</th>
|
||||||
/>
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
|
<tbody>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
|
{registryItems.map((item) => (
|
||||||
{availableItems.map((item) => (
|
<tr key={item.id}>
|
||||||
<div
|
<td>
|
||||||
key={item.id}
|
<div className="flex flex-col gap-1">
|
||||||
className={`card bg-base-100 shadow-xl cursor-pointer ${
|
<div className="flex items-center gap-2">
|
||||||
claimedItems.has(item.id)
|
{item.name}
|
||||||
? "border-4 border-primary"
|
{item.taken && (
|
||||||
: ""
|
<div className="flex items-center gap-2">
|
||||||
}`}
|
<span className="badge badge-success">Taken</span>
|
||||||
onClick={() => handleItemClick(item.id)}
|
<button
|
||||||
>
|
onClick={() => toggleClaimantVisibility(item.id)}
|
||||||
<div className="card-body">
|
className="btn btn-xs btn-ghost"
|
||||||
<h2 className="card-title">{item.name}</h2>
|
>
|
||||||
|
{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 && (
|
{item.link && (
|
||||||
<a
|
<a
|
||||||
href={item.link}
|
href={item.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="link link-primary"
|
className="link link-primary"
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
View Item
|
View
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</td>
|
||||||
</div>
|
<td>{item.taken ? "Claimed" : "Available"}</td>
|
||||||
))}
|
<td>
|
||||||
</div>
|
{!item.taken ? (
|
||||||
|
<input
|
||||||
{claimedItems.size > 0 && (
|
type="checkbox"
|
||||||
<div className="text-center mb-8">
|
className="checkbox"
|
||||||
<button
|
checked={claimedItems.has(item.id)}
|
||||||
onClick={handleSubmit}
|
onChange={() => handleCheckboxChange(item.id)}
|
||||||
className="btn btn-primary"
|
/>
|
||||||
disabled={!claimantName.trim()}
|
) : <input
|
||||||
>
|
type="checkbox"
|
||||||
Claim Selected Items
|
className="checkbox"
|
||||||
</button>
|
disabled
|
||||||
</div>
|
checked={true}
|
||||||
)}
|
/>}
|
||||||
</>
|
</td>
|
||||||
)}
|
</tr>
|
||||||
|
|
||||||
{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>
|
|
||||||
))}
|
))}
|
||||||
|
</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>
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!claimantName.trim()}
|
||||||
|
>
|
||||||
|
Submit Claims
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 toggleShowClaimant = (id: string) => {
|
||||||
const newShowClaimants = new Set(showClaimants);
|
const newShowClaimants = new Set(showClaimants);
|
||||||
if (newShowClaimants.has(id)) {
|
if (newShowClaimants.has(id)) {
|
||||||
@ -111,26 +132,32 @@ const RegistryManager = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4">
|
||||||
<form onSubmit={handleAddItem} className="mb-8">
|
<form onSubmit={handleAddItem} className="mb-8 max-w-2xl mx-auto">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
<input
|
<div className="form-control w-full">
|
||||||
type="text"
|
<input
|
||||||
value={newItem.name}
|
type="text"
|
||||||
onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
|
value={newItem.name}
|
||||||
placeholder="Item name"
|
onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
|
||||||
className="input input-bordered"
|
placeholder="Item name"
|
||||||
required
|
className="input input-bordered w-full"
|
||||||
/>
|
required
|
||||||
<input
|
/>
|
||||||
type="url"
|
</div>
|
||||||
value={newItem.link}
|
<div className="form-control w-full">
|
||||||
onChange={(e) => setNewItem({ ...newItem, link: e.target.value })}
|
<input
|
||||||
placeholder="Item link (optional)"
|
type="url"
|
||||||
className="input input-bordered"
|
value={newItem.link}
|
||||||
/>
|
onChange={(e) => setNewItem({ ...newItem, link: e.target.value })}
|
||||||
<button type="submit" className="btn btn-primary">
|
placeholder="Item link (optional)"
|
||||||
Add Item
|
className="input input-bordered w-full"
|
||||||
</button>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<button type="submit" className="btn btn-primary w-full">
|
||||||
|
Add Item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -185,12 +212,22 @@ const RegistryManager = () => {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<div className="flex gap-2">
|
||||||
onClick={() => handleDeleteItem(item.id)}
|
{item.taken && (
|
||||||
className="btn btn-error btn-sm"
|
<button
|
||||||
>
|
onClick={() => handleReleaseClaim(item.id)}
|
||||||
Delete
|
className="btn btn-warning btn-sm"
|
||||||
</button>
|
>
|
||||||
|
Release Claim
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteItem(item.id)}
|
||||||
|
className="btn btn-error btn-sm"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
@ -4,13 +4,21 @@ import type { RSVPItem } from "../../lib/types";
|
|||||||
import { createProtectedAPIRoute } from "../../utils/auth-middleware";
|
import { createProtectedAPIRoute } from "../../utils/auth-middleware";
|
||||||
|
|
||||||
const objectsToCSV = (data: RSVPItem[]): string => {
|
const objectsToCSV = (data: RSVPItem[]): string => {
|
||||||
const headers = ["name", "dietaryRestrictions", "notes", "timestamp"];
|
const headers = ["name", "attending", "dietaryRestrictions", "notes", "timestamp"];
|
||||||
const csvRows = [headers.join(",")];
|
const csvRows = [headers.join(",")];
|
||||||
|
|
||||||
data.forEach((entry) => {
|
data.forEach((entry) => {
|
||||||
const row = headers.map((header) => {
|
const row = headers.map((header) => {
|
||||||
const field = String(entry[header as keyof RSVPItem]);
|
let field = entry[header as keyof RSVPItem];
|
||||||
const escaped = field.replace(/"/g, '""');
|
// 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}"`;
|
return `"${escaped}"`;
|
||||||
});
|
});
|
||||||
csvRows.push(row.join(","));
|
csvRows.push(row.join(","));
|
||||||
@ -22,18 +30,14 @@ const objectsToCSV = (data: RSVPItem[]): string => {
|
|||||||
const csvToObjects = (csv: string): RSVPItem[] => {
|
const csvToObjects = (csv: string): RSVPItem[] => {
|
||||||
const lines = csv.split("\n");
|
const lines = csv.split("\n");
|
||||||
const headers = lines[0].split(",").map((h) => h.trim());
|
const headers = lines[0].split(",").map((h) => h.trim());
|
||||||
|
const hasAttendingColumn = headers.includes("attending");
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.filter((line) => line.trim())
|
.filter((line) => line.trim())
|
||||||
.map((line) => {
|
.map((line) => {
|
||||||
const values = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || [];
|
const values = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || [];
|
||||||
const entry: Partial<RSVPItem> = {
|
const entry: Partial<RSVPItem> = {};
|
||||||
name: "",
|
|
||||||
dietaryRestrictions: "",
|
|
||||||
notes: "",
|
|
||||||
timestamp: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
headers.forEach((header, index) => {
|
headers.forEach((header, index) => {
|
||||||
let value = values[index] || "";
|
let value = values[index] || "";
|
||||||
@ -41,6 +45,13 @@ const csvToObjects = (csv: string): RSVPItem[] => {
|
|||||||
entry[header as keyof RSVPItem] = value;
|
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;
|
return entry as RSVPItem;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -84,38 +95,29 @@ const handleGet: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
// POST: Submit a new RSVP (requires guest role)
|
// POST: Submit a new RSVP (requires guest role)
|
||||||
const handlePost: APIRoute = async ({ request }) => {
|
const handlePost: APIRoute = async ({ request }) => {
|
||||||
console.log("API endpoint hit - starting request processing");
|
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const FILE_KEY = "rsvp.csv";
|
const FILE_KEY = "rsvp.csv";
|
||||||
|
|
||||||
console.log("Parsing request body");
|
|
||||||
const newRsvp = await request.json();
|
const newRsvp = await request.json();
|
||||||
console.log("Received RSVP data:", newRsvp);
|
|
||||||
|
|
||||||
let existingRsvps: RSVPItem[] = [];
|
let existingRsvps: RSVPItem[] = [];
|
||||||
|
|
||||||
console.log("Attempting to fetch existing RSVPs");
|
|
||||||
const fileContent = await getS3Data<string>(FILE_KEY);
|
const fileContent = await getS3Data<string>(FILE_KEY);
|
||||||
if (fileContent) {
|
if (fileContent) {
|
||||||
existingRsvps = csvToObjects(fileContent);
|
existingRsvps = csvToObjects(fileContent);
|
||||||
console.log("Existing RSVPs loaded:", existingRsvps.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
existingRsvps.push({
|
existingRsvps.push({
|
||||||
...newRsvp,
|
...newRsvp,
|
||||||
|
attending: Boolean(newRsvp.attending),
|
||||||
notes: newRsvp.notes || "",
|
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);
|
const csvContent = objectsToCSV(existingRsvps);
|
||||||
|
|
||||||
await putS3Data(FILE_KEY, csvContent, "text/csv");
|
await putS3Data(FILE_KEY, csvContent, "text/csv");
|
||||||
|
|
||||||
return new Response(JSON.stringify({ success: true }), {
|
return new Response(JSON.stringify({ success: true }), {
|
||||||
@ -128,7 +130,85 @@ const handlePost: APIRoute = async ({ request }) => {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : "Unknown error",
|
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,
|
status: 500,
|
||||||
@ -141,3 +221,4 @@ const handlePost: APIRoute = async ({ request }) => {
|
|||||||
// Export protected routes
|
// Export protected routes
|
||||||
export const GET = createProtectedAPIRoute(handleGet, "admin");
|
export const GET = createProtectedAPIRoute(handleGet, "admin");
|
||||||
export const POST = createProtectedAPIRoute(handlePost, "guest");
|
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">
|
<div class="collapse collapse-plus bg-base-200">
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
Dress Code?
|
Dress Code
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
@ -27,7 +27,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
<div class="collapse collapse-plus bg-base-200">
|
<div class="collapse collapse-plus bg-base-200">
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
Dietary Restrictions?
|
Dietary Restrictions
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
@ -41,7 +41,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
<div class="collapse collapse-plus bg-base-200">
|
<div class="collapse collapse-plus bg-base-200">
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
Alcohol?
|
Alcohol
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
@ -54,7 +54,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
<div class="collapse collapse-plus bg-base-200">
|
<div class="collapse collapse-plus bg-base-200">
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
Childcare?
|
Childcare
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
@ -68,7 +68,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
<div class="collapse collapse-plus bg-base-200">
|
<div class="collapse collapse-plus bg-base-200">
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
Gifts?
|
Gifts
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
@ -99,7 +99,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
<div class="collapse collapse-plus bg-base-200">
|
<div class="collapse collapse-plus bg-base-200">
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
<div class="collapse-title text-xl font-medium">
|
<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>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<div class="text-lg space-y-2">
|
<div class="text-lg space-y-2">
|
||||||
|
@ -10,10 +10,10 @@ import SignIn from "../components/SignIn.tsx";
|
|||||||
|
|
||||||
<div id="content-container" class="hidden">
|
<div id="content-container" class="hidden">
|
||||||
<div class="flex flex-col gap-8 max-w-5xl mx-auto p-6">
|
<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 -->
|
<!-- Header -->
|
||||||
<div class="text-center space-y-4">
|
<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-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>
|
<p class="text-2xl font-semibold">Saturday, June 7, 2025</p>
|
||||||
</div>
|
</div>
|
||||||
@ -21,7 +21,7 @@ import SignIn from "../components/SignIn.tsx";
|
|||||||
<!-- Event Details Cards -->
|
<!-- Event Details Cards -->
|
||||||
<div class="flex flex-col md:flex-row gap-6">
|
<div class="flex flex-col md:flex-row gap-6">
|
||||||
<!-- Ceremony Card -->
|
<!-- 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">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-2xl justify-center">Ceremony</h2>
|
<h2 class="card-title text-2xl justify-center">Ceremony</h2>
|
||||||
<div class="text-center space-y-2">
|
<div class="text-center space-y-2">
|
||||||
@ -40,7 +40,7 @@ import SignIn from "../components/SignIn.tsx";
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reception Card -->
|
<!-- 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">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-2xl justify-center">Reception</h2>
|
<h2 class="card-title text-2xl justify-center">Reception</h2>
|
||||||
<div class="text-center space-y-2">
|
<div class="text-center space-y-2">
|
||||||
|
Reference in New Issue
Block a user