This commit is contained in:
2025-02-07 02:56:17 -06:00
parent 6b01c165a9
commit a1363ee4b5
5 changed files with 169 additions and 74 deletions

View File

@ -6,29 +6,31 @@ const RegistryList = () => {
const [claimedItems, setClaimedItems] = useState<Set<string>>(new Set()); 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 [showClaimants, setShowClaimants] = useState<Set<string>>(new Set());
useEffect(() => { 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(); 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 handleCheckboxChange = (itemId: string) => { const handleCheckboxChange = (itemId: string) => {
const newClaimedItems = new Set(claimedItems); const newClaimedItems = new Set(claimedItems);
if (newClaimedItems.has(itemId)) { if (newClaimedItems.has(itemId)) {
@ -39,31 +41,42 @@ const RegistryList = () => {
setClaimedItems(newClaimedItems); setClaimedItems(newClaimedItems);
}; };
const handleSubmit = async () => { const toggleClaimantVisibility = (itemId: string) => {
// Prepare updates for claimed items const newShowClaimants = new Set(showClaimants);
const updates = registryItems.filter((item) => if (newShowClaimants.has(itemId)) {
claimedItems.has(item.id) newShowClaimants.delete(itemId);
); } else {
newShowClaimants.add(itemId);
}
setShowClaimants(newShowClaimants);
};
const handleSubmit = async () => {
if (!claimantName.trim()) {
setError("Please enter your name before claiming items");
return;
}
const updates = registryItems.filter((item) => claimedItems.has(item.id));
// Optimistically update the UI
const updatedRegistryItems = registryItems.map((item) => { const updatedRegistryItems = registryItems.map((item) => {
if (claimedItems.has(item.id)) { if (claimedItems.has(item.id)) {
return { ...item, taken: true }; return { ...item, taken: true, claimedBy: claimantName };
} }
return item; return item;
}); });
setRegistryItems(updatedRegistryItems); setRegistryItems(updatedRegistryItems);
setClaimedItems(new Set()); // Clear claimed items after submission setClaimedItems(new Set());
setClaimantName("");
try { try {
// Send updates to the server
for (const item of updates) { for (const item of updates) {
const response = await fetch(`/api/registry/${item.id}`, { const response = await fetch(`/api/registry/${item.id}`, {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ taken: true }), body: JSON.stringify({ taken: true, claimedBy: claimantName }),
}); });
if (!response.ok) { if (!response.ok) {
@ -85,9 +98,7 @@ const RegistryList = () => {
} }
if (error) { if (error) {
return ( return <div className="text-red-500 text-center">Error: {error}</div>;
<div className="text-red-500 text-center">Error: {error}</div>
);
} }
return ( return (
@ -99,17 +110,37 @@ const RegistryList = () => {
<tr> <tr>
<th>Item</th> <th>Item</th>
<th>Link</th> <th>Link</th>
<th>Status</th>
<th>Claim</th> <th>Claim</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{registryItems.map((item) => ( {registryItems.map((item) => (
<tr key={item.id} className={item.taken ? "opacity-50" : ""}> <tr key={item.id}>
<td> <td>
{item.name} <div className="flex flex-col gap-1">
{item.taken && ( <div className="flex items-center gap-2">
<span className="badge badge-success ml-2">Taken</span> {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>
<td> <td>
{item.link && ( {item.link && (
@ -123,28 +154,51 @@ const RegistryList = () => {
</a> </a>
)} )}
</td> </td>
<td>{item.taken ? "Claimed" : "Available"}</td>
<td> <td>
{!item.taken && ( {!item.taken ? (
<input <input
type="checkbox" type="checkbox"
className="checkbox" className="checkbox"
checked={claimedItems.has(item.id)} checked={claimedItems.has(item.id)}
onChange={() => handleCheckboxChange(item.id)} onChange={() => handleCheckboxChange(item.id)}
/> />
)} ) : <input
type="checkbox"
className="checkbox"
disabled
checked={true}
/>}
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
<button {claimedItems.size > 0 && (
className="btn btn-primary mt-4" <div className="mt-4 flex flex-col gap-4">
onClick={handleSubmit} <div className="form-control">
disabled={claimedItems.size === 0} <label className="label">
> <span className="label-text">Your Name</span>
Submit Claims </label>
</button> <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> </div>
); );
}; };

View File

@ -6,6 +6,7 @@ const RegistryManager = () => {
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 [newItem, setNewItem] = useState({ name: "", link: "" }); const [newItem, setNewItem] = useState({ name: "", link: "" });
const [showClaimants, setShowClaimants] = useState<Set<string>>(new Set());
useEffect(() => { useEffect(() => {
fetchRegistryItems(); fetchRegistryItems();
@ -46,7 +47,6 @@ const RegistryManager = () => {
throw new Error(`Failed to add item: ${response.statusText}`); throw new Error(`Failed to add item: ${response.statusText}`);
} }
// Clear form and refresh list
setNewItem({ name: "", link: "" }); setNewItem({ name: "", link: "" });
await fetchRegistryItems(); await fetchRegistryItems();
} catch (error: any) { } catch (error: any) {
@ -67,7 +67,6 @@ const RegistryManager = () => {
throw new Error(`Failed to delete item: ${response.statusText}`); throw new Error(`Failed to delete item: ${response.statusText}`);
} }
// Refresh the list after deletion
await fetchRegistryItems(); await fetchRegistryItems();
} catch (error: any) { } catch (error: any) {
setError(error.message); setError(error.message);
@ -75,6 +74,16 @@ const RegistryManager = () => {
} }
}; };
const toggleClaimantVisibility = (itemId: string) => {
const newShowClaimants = new Set(showClaimants);
if (newShowClaimants.has(itemId)) {
newShowClaimants.delete(itemId);
} else {
newShowClaimants.add(itemId);
}
setShowClaimants(newShowClaimants);
};
if (loading) { if (loading) {
return <div className="text-center">Loading registry items...</div>; return <div className="text-center">Loading registry items...</div>;
} }
@ -85,9 +94,6 @@ const RegistryManager = () => {
return ( return (
<div className="container mx-auto p-4"> <div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Registry Manager</h1>
{/* Add New Item Form */}
<form onSubmit={handleAddItem} className="mb-8"> <form onSubmit={handleAddItem} className="mb-8">
<div className="card bg-base-100 shadow-xl"> <div className="card bg-base-100 shadow-xl">
<div className="card-body"> <div className="card-body">
@ -130,7 +136,6 @@ const RegistryManager = () => {
</div> </div>
</form> </form>
{/* Registry Items List */}
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="table w-full"> <table className="table w-full">
<thead> <thead>
@ -144,7 +149,35 @@ const RegistryManager = () => {
<tbody> <tbody>
{registryItems.map((item) => ( {registryItems.map((item) => (
<tr key={item.id}> <tr key={item.id}>
<td>{item.name}</td> <td>
<div className="flex items-center gap-2">
{item.name}
{item.taken && (
<>
<span className="badge badge-success">Taken</span>
{item.claimedBy && (
<button
onClick={() => toggleClaimantVisibility(item.id)}
className="btn btn-ghost btn-xs"
>
{showClaimants.has(item.id) ? (
<i className="fas fa-eye-slash" />
) : (
<i className="fas fa-eye" />
)}
</button>
)}
</>
)}
</div>
{item.taken &&
item.claimedBy &&
showClaimants.has(item.id) && (
<div className="text-sm text-gray-500">
Claimed by: {item.claimedBy}
</div>
)}
</td>
<td> <td>
{item.link && ( {item.link && (
<a <a
@ -157,13 +190,7 @@ const RegistryManager = () => {
</a> </a>
)} )}
</td> </td>
<td> <td>{item.taken ? "Claimed" : "Available"}</td>
{item.taken ? (
<span className="badge badge-success">Taken</span>
) : (
<span className="badge badge-info">Available</span>
)}
</td>
<td> <td>
<button <button
className="btn btn-error btn-sm" className="btn btn-error btn-sm"

View File

@ -3,6 +3,7 @@ export interface RegistryItem {
name: string; name: string;
taken: boolean; taken: boolean;
link?: string; link?: string;
claimedBy?: string;
} }
export interface RSVPItem { export interface RSVPItem {

View File

@ -2,7 +2,7 @@ import type { APIRoute } from "astro";
import { getS3Data, putS3Data } from "../../../lib/s3"; import { getS3Data, putS3Data } from "../../../lib/s3";
import type { RegistryItem } from "../../../lib/types"; import type { RegistryItem } from "../../../lib/types";
const REGISTRY_FILE_KEY = "baby-registry.json"; const REGISTRY_FILE_KEY = "registry.json";
// GET: Get a specific registry item by ID // GET: Get a specific registry item by ID
export const GET: APIRoute = async ({ params }) => { export const GET: APIRoute = async ({ params }) => {
@ -50,7 +50,7 @@ export const PUT: APIRoute = async ({ request, params }) => {
} }
const body = await request.json(); const body = await request.json();
const { name, taken, link } = body; const { taken, claimedBy } = body;
const registry = await getS3Data<RegistryItem[]>(REGISTRY_FILE_KEY) || []; const registry = await getS3Data<RegistryItem[]>(REGISTRY_FILE_KEY) || [];
const itemIndex = registry.findIndex((item) => item.id === id); const itemIndex = registry.findIndex((item) => item.id === id);
@ -62,12 +62,10 @@ export const PUT: APIRoute = async ({ request, params }) => {
); );
} }
// Update the item with the provided values
registry[itemIndex] = { registry[itemIndex] = {
...registry[itemIndex], ...registry[itemIndex],
name: name !== undefined ? name : registry[itemIndex].name,
taken: taken !== undefined ? taken : registry[itemIndex].taken, taken: taken !== undefined ? taken : registry[itemIndex].taken,
link: link !== undefined ? link : registry[itemIndex].link, claimedBy: claimedBy !== undefined ? claimedBy : registry[itemIndex].claimedBy,
}; };
await putS3Data(REGISTRY_FILE_KEY, registry); await putS3Data(REGISTRY_FILE_KEY, registry);
@ -85,39 +83,54 @@ export const PUT: APIRoute = async ({ request, params }) => {
} }
}; };
// DELETE: Delete a registry item // DELETE: Delete a registry item
export const DELETE: APIRoute = async ({ params }) => { export const DELETE: APIRoute = async ({ params }) => {
const headers = {
"Content-Type": "application/json",
};
try { try {
const { id } = params; const { id } = params;
if (!id) { if (!id) {
return new Response( return new Response(
JSON.stringify({ success: false, error: "Item ID is required" }), JSON.stringify({ success: false, error: "Item ID is required" }),
{ status: 400, headers: { "Content-Type": "application/json" } } { status: 400, headers }
); );
} }
// Get current registry data
const registry = await getS3Data<RegistryItem[]>(REGISTRY_FILE_KEY) || []; const registry = await getS3Data<RegistryItem[]>(REGISTRY_FILE_KEY) || [];
// Find the item index
const itemIndex = registry.findIndex((item) => item.id === id); const itemIndex = registry.findIndex((item) => item.id === id);
if (itemIndex === -1) { if (itemIndex === -1) {
return new Response( return new Response(
JSON.stringify({ success: false, error: "Item not found" }), JSON.stringify({ success: false, error: "Item not found" }),
{ status: 404, headers: { "Content-Type": "application/json" } } { status: 404, headers }
); );
} }
registry.splice(itemIndex, 1); // Remove the item from the array // Remove the item
registry.splice(itemIndex, 1);
// Save the updated registry
await putS3Data(REGISTRY_FILE_KEY, registry); await putS3Data(REGISTRY_FILE_KEY, registry);
return new Response(JSON.stringify({ success: true }), { return new Response(JSON.stringify({ success: true }), {
status: 204, // No Content status: 200,
headers: { "Content-Type": "application/json" }, headers,
}); });
} catch (error: any) { } catch (error) {
console.error("Error deleting registry item:", error.message); console.error("Error deleting registry item:", error);
return new Response( return new Response(
JSON.stringify({ success: false, error: "Failed to delete item" }), JSON.stringify({
{ status: 500, headers: { "Content-Type": "application/json" } } success: false,
error: "Failed to delete item",
details: error instanceof Error ? error.message : "Unknown error",
}),
{ status: 500, headers }
); );
} }
}; };

View File

@ -3,7 +3,7 @@ import { getS3Data, putS3Data } from "../../../lib/s3";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import type { RegistryItem } from "../../../lib/types"; import type { RegistryItem } from "../../../lib/types";
const REGISTRY_FILE_KEY = "baby-registry.json"; const REGISTRY_FILE_KEY = "registry.json";
// GET: List all registry items // GET: List all registry items
export const GET: APIRoute = async () => { export const GET: APIRoute = async () => {