refactor: remove enhanced seating chart component and initialize admin data loading states to false.

This commit is contained in:
2026-01-29 09:22:01 +02:00
parent 641bef46cb
commit 5f22444332
5 changed files with 83 additions and 968 deletions

View File

@@ -476,7 +476,7 @@ function AdminHeader({ onSettingsOpen, onAddGuest }: { onSettingsOpen: () => voi
<Settings className="h-4 w-4 mr-2" />
Settings
</Button>
<Button variant="default" size="sm" onClick={onAddGuest} className="bg-wedding-evergreen hover:bg-wedding-moss">
<Button variant="default" size="sm" onClick={onAddGuest} className="bg-wedding-moss hover:bg-wedding-sage">
<Plus className="h-4 w-4 mr-2" />
Add Guest
</Button>
@@ -510,7 +510,7 @@ function AdminNavbar() {
function NavTrigger({ value, icon: Icon, label }: { value: string; icon: React.ComponentType<{ className?: string }>; label: string }) {
return (
<TabsTrigger value={value} className="rounded-lg data-[state=active]:bg-wedding-evergreen data-[state=active]:text-white hover:bg-wedding-sage/10 hover:text-wedding-evergreen transition-all">
<TabsTrigger value={value} className="rounded-lg data-[state=active]:bg-wedding-moss data-[state=active]:text-white hover:bg-wedding-sage/10 hover:text-wedding-moss transition-all">
<div className="flex items-center gap-2 px-2 py-1">
<Icon className="h-4 w-4 text-inherit" />
<span className="hidden sm:inline whitespace-nowrap">{label}</span>

View File

@@ -89,8 +89,8 @@ export function AdminSidebar({ stats, onLogout }: AdminSidebarProps) {
"flex items-center justify-between gap-3 px-3 py-2 rounded-lg transition-all font-body group relative",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-wedding-evergreen/50 focus-visible:ring-offset-2",
active
? "bg-wedding-sage/20 text-wedding-evergreen shadow-sm"
: "text-wedding-ink/70 hover:bg-wedding-sage/10 hover:text-wedding-evergreen"
? "bg-wedding-sage/20 text-wedding-moss shadow-sm"
: "text-wedding-ink/70 hover:bg-wedding-sage/10 hover:text-wedding-moss"
)}
>
{active && (
@@ -134,19 +134,19 @@ export function AdminSidebar({ stats, onLogout }: AdminSidebarProps) {
<div className="space-y-1">
<Link
href="/admin?tab=guest-requests"
className="block px-3 py-2 text-xs font-body text-wedding-ink/70 hover:text-wedding-evergreen hover:bg-wedding-sage/10 rounded-lg transition-colors"
className="block px-3 py-2 text-xs font-body text-wedding-ink/70 hover:text-wedding-moss hover:bg-wedding-sage/10 rounded-lg transition-colors"
>
View Guest Requests
</Link>
<Link
href="/admin?tab=rsvp"
className="block px-3 py-2 text-xs font-body text-wedding-ink/70 hover:text-wedding-evergreen hover:bg-wedding-sage/10 rounded-lg transition-colors"
className="block px-3 py-2 text-xs font-body text-wedding-ink/70 hover:text-wedding-moss hover:bg-wedding-sage/10 rounded-lg transition-colors"
>
Send RSVP Reminder
</Link>
<Link
href="/admin?tab=export"
className="block px-3 py-2 text-xs font-body text-wedding-ink/70 hover:text-wedding-evergreen hover:bg-wedding-sage/10 rounded-lg transition-colors"
className="block px-3 py-2 text-xs font-body text-wedding-ink/70 hover:text-wedding-moss hover:bg-wedding-sage/10 rounded-lg transition-colors"
>
Export Guest List
</Link>

View File

@@ -73,23 +73,23 @@ export function useAdminData(): UseAdminDataReturn {
// Stats
const [stats, setStats] = useState<any>(null);
const [statsLoading, setStatsLoading] = useState(true);
const [statsLoading, setStatsLoading] = useState(false);
// Recent Activity
const [recentActivity, setRecentActivity] = useState<any[]>([]);
const [recentActivityLoading, setRecentActivityLoading] = useState(true);
const [recentActivityLoading, setRecentActivityLoading] = useState(false);
// RSVP data
const [rsvpData, setRsvpData] = useState<any>(null);
const [rsvpLoading, setRsvpLoading] = useState(true);
const [rsvpLoading, setRsvpLoading] = useState(false);
// Tributes data
const [tributesData, setTributesData] = useState<any>(null);
const [tributesLoading, setTributesLoading] = useState(true);
const [tributesLoading, setTributesLoading] = useState(false);
// Guestbook data
const [guestbookEntries, setGuestbookEntries] = useState<any[]>([]);
const [guestbookLoading, setGuestbookLoading] = useState(true);
const [guestbookLoading, setGuestbookLoading] = useState(false);
// Music data
const [musicRequests, setMusicRequests] = useState<any[]>([]);
@@ -100,7 +100,7 @@ export function useAdminData(): UseAdminDataReturn {
// Who's Who data
const [whosWhoData, setWhosWhoData] = useState<any>(null);
const [whosWhoLoading, setWhosWhoLoading] = useState(true);
const [whosWhoLoading, setWhosWhoLoading] = useState(false);
// Fetch Stats
const fetchStats = useCallback(async () => {

View File

@@ -1,885 +0,0 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import {
DndContext,
DragOverlay,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
type DragEndEvent,
type DragStartEvent,
} from "@dnd-kit/core";
import { useSortable } from "@dnd-kit/sortable";
import { useDroppable } from "@dnd-kit/core";
import { CSS } from "@dnd-kit/utilities";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import {
Users,
GripVertical,
Search,
Plus,
Download,
Edit3,
Trash2,
AlertTriangle,
Utensils,
Printer,
LayoutGrid,
List,
X,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useToast } from "@/components/ui/use-toast";
import { useRealtimeSync } from "@/hooks/use-realtime-sync";
interface Table {
id: string;
name: string;
capacity: number;
guests: Guest[];
}
interface Guest {
id: string;
name: string;
surname?: string;
maxPax: number;
tableId?: string;
dietaryRestrictions?: string;
relationship?: string;
isAttending?: boolean | null;
}
// Draggable Guest Component
function DraggableGuest({
guest,
isOverlay = false,
}: {
guest: Guest;
isOverlay?: boolean;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: guest.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
<div
ref={setNodeRef}
style={isOverlay ? undefined : style}
{...attributes}
{...listeners}
className={cn(
"flex items-center gap-2 p-2 bg-white rounded-lg border border-wedding-sage/20 cursor-grab active:cursor-grabbing hover:border-wedding-sage/40 transition-colors",
isDragging && "shadow-lg",
isOverlay && "shadow-xl"
)}
>
<GripVertical className="h-4 w-4 text-wedding-moss/40" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">
{guest.name} {guest.surname}
</p>
<div className="flex items-center gap-1">
<span className="text-xs text-wedding-moss/60">
{guest.maxPax} {guest.maxPax === 1 ? "guest" : "guests"}
</span>
{guest.dietaryRestrictions && (
<Badge variant="outline" className="text-xs px-1 py-0">
<Utensils className="h-3 w-3 mr-1" />
Diet
</Badge>
)}
</div>
</div>
</div>
);
}
// Droppable Table Component
function DroppableTable({
table,
onEdit,
onDelete,
}: {
table: Table;
onEdit: () => void;
onDelete: () => void;
}) {
const { setNodeRef, isOver } = useDroppable({ id: table.id });
const currentOccupancy = table.guests.reduce((sum, g) => sum + g.maxPax, 0);
const isOverCapacity = currentOccupancy > table.capacity;
const dietaryCount = table.guests.filter((g) => g.dietaryRestrictions).length;
return (
<Card
ref={setNodeRef}
variant="glass"
className={cn(
"transition-all",
isOver && "ring-2 ring-wedding-sage bg-wedding-sage/5",
isOverCapacity && "border-amber-400"
)}
>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="font-heading text-lg">{table.name}</CardTitle>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onEdit}>
<Edit3 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-red-500 hover:text-red-600"
onClick={onDelete}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
<CardDescription className="font-body flex items-center gap-2">
<Users className="h-4 w-4" />
<span
className={cn(
isOverCapacity && "text-amber-600 font-medium"
)}
>
{currentOccupancy}/{table.capacity}
</span>
{isOverCapacity && (
<AlertTriangle className="h-4 w-4 text-amber-500" />
)}
{dietaryCount > 0 && (
<Badge variant="secondary" className="text-xs">
<Utensils className="h-3 w-3 mr-1" />
{dietaryCount}
</Badge>
)}
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 min-h-[100px]">
{table.guests.length === 0 ? (
<p className="text-sm text-wedding-moss/50 text-center py-4">
Drop guests here
</p>
) : (
table.guests.map((guest) => (
<DraggableGuest key={guest.id} guest={guest} />
))
)}
</CardContent>
</Card>
);
}
export function SeatingChartEnhanced() {
const { toast } = useToast();
const [tables, setTables] = useState<Table[]>([]);
const [unseatedGuests, setUnseatedGuests] = useState<Guest[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const [activeId, setActiveId] = useState<string | null>(null);
const [showAddTable, setShowAddTable] = useState(false);
const [showEditTable, setShowEditTable] = useState<Table | null>(null);
const [newTableName, setNewTableName] = useState("");
const [newTableCapacity, setNewTableCapacity] = useState(8);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor)
);
useEffect(() => {
fetchData();
}, []);
// Subscribe to real-time updates for seating, guest, and table changes
useRealtimeSync({
eventTypes: ['seating', 'guest', 'table'],
onUpdate: (event) => {
if (
event.type === 'seating' ||
(event.type === 'guest' && event.action === 'update' && (event.data?.tableId !== undefined || event.data?.rsvpStatus)) ||
event.type === 'table'
) {
fetchData();
}
},
debounceMs: 1000,
});
const fetchData = async () => {
try {
setLoading(true);
const [guestsRes, tablesRes] = await Promise.all([
fetch("/api/admin/guests", { credentials: "include" }),
fetch("/api/admin/tables", { credentials: "include" }),
]);
const guestsData = await guestsRes.json();
const tablesData = await tablesRes.json();
// Process tables
const processedTables: Table[] = (tablesData.tables || []).map(
(t: any) => ({
id: t.id,
name: t.name,
capacity: t.capacity || 8,
guests: [],
})
);
// Process guests
const allGuests: Guest[] = (guestsData.guests || [])
.filter((g: any) => g.isAttending === true)
.map((g: any) => ({
id: g.id,
name: g.name,
surname: g.surname,
maxPax: g.maxPax || 1,
tableId: g.tableId,
dietaryRestrictions: g.dietaryRestrictions,
relationship: g.relationship,
isAttending: g.isAttending,
}));
// Assign guests to tables
allGuests.forEach((guest) => {
if (guest.tableId) {
const table = processedTables.find((t) => t.id === guest.tableId);
if (table) {
table.guests.push(guest);
} else {
setUnseatedGuests((prev) => [...prev, guest]);
}
}
});
// Unseated guests
const unseated = allGuests.filter(
(g) => !g.tableId || !processedTables.find((t) => t.id === g.tableId)
);
setTables(processedTables);
setUnseatedGuests(unseated);
} catch (error) {
console.error("Failed to fetch seating data:", error);
toast({
variant: "error",
title: "Failed to load seating data",
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setLoading(false);
}
};
// Filter unseated guests by search
const filteredUnseated = useMemo(() => {
if (!searchQuery) return unseatedGuests;
const query = searchQuery.toLowerCase();
return unseatedGuests.filter(
(g) =>
g.name.toLowerCase().includes(query) ||
g.surname?.toLowerCase().includes(query)
);
}, [unseatedGuests, searchQuery]);
// Dietary summary
const dietarySummary = useMemo(() => {
const allGuests = [
...unseatedGuests,
...tables.flatMap((t) => t.guests),
];
const restrictions: Record<string, number> = {};
allGuests.forEach((guest) => {
if (guest.dietaryRestrictions) {
const items = guest.dietaryRestrictions.split(",").map((s) => s.trim());
items.forEach((item) => {
restrictions[item] = (restrictions[item] || 0) + 1;
});
}
});
return Object.entries(restrictions)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
}, [tables, unseatedGuests]);
// Handle drag events
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string);
};
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
setActiveId(null);
if (!over) return;
const guestId = active.id as string;
const targetTableId = over.id as string;
// Find guest
let guest: Guest | undefined;
let sourceTableId: string | null = null;
// Check unseated
const unseatedIndex = unseatedGuests.findIndex((g) => g.id === guestId);
if (unseatedIndex >= 0) {
guest = unseatedGuests[unseatedIndex];
} else {
// Check tables
for (const table of tables) {
const idx = table.guests.findIndex((g) => g.id === guestId);
if (idx >= 0) {
guest = table.guests[idx];
sourceTableId = table.id;
break;
}
}
}
if (!guest) return;
// If dropping on "unseated" area
if (targetTableId === "unseated") {
if (sourceTableId) {
// Move from table to unseated
setTables((prev) =>
prev.map((t) =>
t.id === sourceTableId
? { ...t, guests: t.guests.filter((g) => g.id !== guestId) }
: t
)
);
setUnseatedGuests((prev) => [...prev, { ...guest!, tableId: undefined }]);
// Update in database
await updateGuestTable(guestId, null);
}
return;
}
// Moving to a table
const targetTable = tables.find((t) => t.id === targetTableId);
if (!targetTable) return;
// Check capacity
const currentOccupancy = targetTable.guests.reduce(
(sum, g) => sum + g.maxPax,
0
);
if (currentOccupancy + guest.maxPax > targetTable.capacity) {
toast({
variant: "error",
title: "Table is full",
description: `${targetTable.name} doesn't have enough capacity`,
});
return;
}
// Update state
if (unseatedIndex >= 0) {
setUnseatedGuests((prev) => prev.filter((g) => g.id !== guestId));
} else if (sourceTableId) {
setTables((prev) =>
prev.map((t) =>
t.id === sourceTableId
? { ...t, guests: t.guests.filter((g) => g.id !== guestId) }
: t
)
);
}
setTables((prev) =>
prev.map((t) =>
t.id === targetTableId
? { ...t, guests: [...t.guests, { ...guest!, tableId: targetTableId }] }
: t
)
);
// Update in database
await updateGuestTable(guestId, targetTableId);
};
const updateGuestTable = async (guestId: string, tableId: string | null) => {
try {
const response = await fetch(`/api/admin/guests/${guestId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ tableId }),
});
if (!response.ok) {
throw new Error("Failed to update guest table");
}
} catch (error) {
toast({
variant: "error",
title: "Failed to save",
description: "Could not update guest's table assignment",
});
// Refresh to restore correct state
fetchData();
}
};
// Add table
const handleAddTable = async () => {
if (!newTableName.trim()) return;
try {
const response = await fetch("/api/admin/tables", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
name: newTableName,
capacity: newTableCapacity,
}),
});
if (!response.ok) throw new Error("Failed to create table");
const result = await response.json();
setTables((prev) => [
...prev,
{ id: result.table.id, name: newTableName, capacity: newTableCapacity, guests: [] },
]);
setShowAddTable(false);
setNewTableName("");
setNewTableCapacity(8);
toast({
variant: "success",
title: "Table created",
description: `${newTableName} has been added`,
});
} catch (error) {
toast({
variant: "error",
title: "Failed to create table",
description: error instanceof Error ? error.message : "Unknown error",
});
}
};
// Delete table
const handleDeleteTable = async (tableId: string) => {
const table = tables.find((t) => t.id === tableId);
if (!table) return;
if (table.guests.length > 0) {
toast({
variant: "error",
title: "Cannot delete table",
description: "Please remove all guests from the table first",
});
return;
}
try {
const response = await fetch(`/api/admin/tables/${tableId}`, {
method: "DELETE",
credentials: "include",
});
if (!response.ok) throw new Error("Failed to delete table");
setTables((prev) => prev.filter((t) => t.id !== tableId));
toast({
variant: "success",
title: "Table deleted",
description: `${table.name} has been removed`,
});
} catch (error) {
toast({
variant: "error",
title: "Failed to delete table",
description: error instanceof Error ? error.message : "Unknown error",
});
}
};
// Export seating chart
const handleExport = async () => {
try {
const response = await fetch("/api/admin/seating/export", {
credentials: "include",
});
if (!response.ok) throw new Error("Export failed");
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `seating-chart-${new Date().toISOString().split("T")[0]}.pdf`;
a.click();
window.URL.revokeObjectURL(url);
toast({
variant: "success",
title: "Export complete",
description: "Seating chart has been downloaded",
});
} catch (error) {
toast({
variant: "error",
title: "Export failed",
description: error instanceof Error ? error.message : "Unknown error",
});
}
};
// Find active guest for drag overlay
const activeGuest = useMemo(() => {
if (!activeId) return null;
const unseated = unseatedGuests.find((g) => g.id === activeId);
if (unseated) return unseated;
for (const table of tables) {
const guest = table.guests.find((g) => g.id === activeId);
if (guest) return guest;
}
return null;
}, [activeId, unseatedGuests, tables]);
if (loading) {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-48" />
<div className="grid gap-4 md:grid-cols-3">
{[...Array(6)].map((_, i) => (
<Skeleton key={i} className="h-48" />
))}
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h2 className="text-2xl font-heading text-wedding-evergreen">
Seating Chart
</h2>
<p className="text-wedding-moss/70 font-body">
Drag and drop guests to assign tables
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => setShowAddTable(true)}>
<Plus className="h-4 w-4 mr-1" />
Add Table
</Button>
<Button variant="outline" size="sm" onClick={handleExport}>
<Printer className="h-4 w-4 mr-1" />
Print
</Button>
<div className="flex border rounded-lg overflow-hidden">
<Button
variant={viewMode === "grid" ? "default" : "ghost"}
size="sm"
className="rounded-none"
onClick={() => setViewMode("grid")}
>
<LayoutGrid className="h-4 w-4" />
</Button>
<Button
variant={viewMode === "list" ? "default" : "ghost"}
size="sm"
className="rounded-none"
onClick={() => setViewMode("list")}
>
<List className="h-4 w-4" />
</Button>
</div>
</div>
</div>
{/* Dietary Summary */}
{dietarySummary.length > 0 && (
<Card variant="glass">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-heading flex items-center gap-2">
<Utensils className="h-4 w-4" />
Dietary Requirements Summary
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{dietarySummary.map(([restriction, count]) => (
<Badge key={restriction} variant="secondary">
{restriction}: {count}
</Badge>
))}
</div>
</CardContent>
</Card>
)}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="grid gap-6 lg:grid-cols-4">
{/* Unseated Guests */}
<div className="lg:col-span-1">
<Card variant="glass" className="sticky top-4">
<CardHeader className="pb-2">
<CardTitle className="font-heading text-lg flex items-center gap-2">
<Users className="h-5 w-5" />
Unseated Guests
</CardTitle>
<CardDescription className="font-body">
{filteredUnseated.length} guests to assign
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-wedding-moss/40" />
<Input
placeholder="Search guests..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<div
className="space-y-2 max-h-[60vh] overflow-y-auto pr-2"
>
{filteredUnseated.length === 0 ? (
<p className="text-sm text-wedding-moss/50 text-center py-4">
{searchQuery ? "No guests found" : "All guests are seated!"}
</p>
) : (
filteredUnseated.map((guest) => (
<DraggableGuest key={guest.id} guest={guest} />
))
)}
</div>
</CardContent>
</Card>
</div>
{/* Tables */}
<div className="lg:col-span-3">
<div
className={cn(
"grid gap-4",
viewMode === "grid" ? "md:grid-cols-2 xl:grid-cols-3" : "grid-cols-1"
)}
>
{tables.map((table) => (
<DroppableTable
key={table.id}
table={table}
onEdit={() => setShowEditTable(table)}
onDelete={() => handleDeleteTable(table.id)}
/>
))}
</div>
</div>
</div>
{/* Drag Overlay */}
<DragOverlay>
{activeGuest && <DraggableGuest guest={activeGuest} isOverlay />}
</DragOverlay>
</DndContext>
{/* Add Table Dialog */}
<Dialog open={showAddTable} onOpenChange={setShowAddTable}>
<DialogContent>
<DialogHeader>
<DialogTitle className="font-heading">Add New Table</DialogTitle>
<DialogDescription className="font-body">
Create a new table for your seating chart
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="tableName">Table Name</Label>
<Input
id="tableName"
value={newTableName}
onChange={(e) => setNewTableName(e.target.value)}
placeholder="e.g., Table 1, Head Table, Family Table"
/>
</div>
<div className="space-y-2">
<Label htmlFor="capacity">Capacity</Label>
<Input
id="capacity"
type="number"
min={1}
max={20}
value={newTableCapacity}
onChange={(e) => setNewTableCapacity(parseInt(e.target.value) || 8)}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowAddTable(false)}>
Cancel
</Button>
<Button onClick={handleAddTable}>Create Table</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Edit Table Dialog */}
<Dialog open={!!showEditTable} onOpenChange={() => setShowEditTable(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle className="font-heading">Edit Table</DialogTitle>
<DialogDescription className="font-body">
Update table details
</DialogDescription>
</DialogHeader>
{showEditTable && (
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="editTableName">Table Name</Label>
<Input
id="editTableName"
defaultValue={showEditTable.name}
onChange={(e) =>
setShowEditTable({ ...showEditTable, name: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="editCapacity">Capacity</Label>
<Input
id="editCapacity"
type="number"
min={1}
max={20}
defaultValue={showEditTable.capacity}
onChange={(e) =>
setShowEditTable({
...showEditTable,
capacity: parseInt(e.target.value) || 8,
})
}
/>
</div>
{/* Dietary summary for this table */}
{showEditTable.guests.some((g) => g.dietaryRestrictions) && (
<div className="space-y-2">
<Label>Dietary Requirements</Label>
<div className="flex flex-wrap gap-1">
{showEditTable.guests
.filter((g) => g.dietaryRestrictions)
.map((g) => (
<Badge key={g.id} variant="outline">
{g.name}: {g.dietaryRestrictions}
</Badge>
))}
</div>
</div>
)}
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setShowEditTable(null)}>
Cancel
</Button>
<Button
onClick={async () => {
if (!showEditTable) return;
try {
await fetch(`/api/admin/tables/${showEditTable.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
name: showEditTable.name,
capacity: showEditTable.capacity,
}),
});
setTables((prev) =>
prev.map((t) =>
t.id === showEditTable.id
? { ...t, name: showEditTable.name, capacity: showEditTable.capacity }
: t
)
);
setShowEditTable(null);
toast({
variant: "success",
title: "Table updated",
});
} catch (error) {
toast({
variant: "error",
title: "Failed to update table",
});
}
}}
>
Save Changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -777,21 +777,21 @@ export default function GuestDashboard() {
{ACT_2_SECTION_IDS.map((id) => {
const { component: Component, lazy } = SECTION_BY_ID[id];
return (
<StaggeredItem key={id}>
<motion.section
id={id}
variants={itemVariants as any}
className="scroll-mt-24"
>
{lazy ? (
<LazySectionWrapper>
<StaggeredItem key={id}>
<motion.section
id={id}
variants={itemVariants as any}
className="scroll-mt-24"
>
{lazy ? (
<LazySectionWrapper>
<Component />
</LazySectionWrapper>
) : (
<Component />
</LazySectionWrapper>
) : (
<Component />
)}
</motion.section>
</StaggeredItem>
)}
</motion.section>
</StaggeredItem>
);
})}
</StaggeredList>
@@ -808,21 +808,21 @@ export default function GuestDashboard() {
{ACT_3_SECTION_IDS.map((id) => {
const { component: Component, lazy } = SECTION_BY_ID[id];
return (
<StaggeredItem key={id}>
<motion.section
id={id}
variants={itemVariants as any}
className="scroll-mt-24"
>
{lazy ? (
<LazySectionWrapper>
<StaggeredItem key={id}>
<motion.section
id={id}
variants={itemVariants as any}
className="scroll-mt-24"
>
{lazy ? (
<LazySectionWrapper>
<Component />
</LazySectionWrapper>
) : (
<Component />
</LazySectionWrapper>
) : (
<Component />
)}
</motion.section>
</StaggeredItem>
)}
</motion.section>
</StaggeredItem>
);
})}
</StaggeredList>
@@ -839,21 +839,21 @@ export default function GuestDashboard() {
{ACT_4_SECTION_IDS.map((id) => {
const { component: Component, lazy } = SECTION_BY_ID[id];
return (
<StaggeredItem key={id}>
<motion.section
id={id}
variants={itemVariants as any}
className="scroll-mt-24"
>
{lazy ? (
<LazySectionWrapper>
<StaggeredItem key={id}>
<motion.section
id={id}
variants={itemVariants as any}
className="scroll-mt-24"
>
{lazy ? (
<LazySectionWrapper>
<Component />
</LazySectionWrapper>
) : (
<Component />
</LazySectionWrapper>
) : (
<Component />
)}
</motion.section>
</StaggeredItem>
)}
</motion.section>
</StaggeredItem>
);
})}
</StaggeredList>
@@ -870,21 +870,21 @@ export default function GuestDashboard() {
{ACT_5_SECTION_IDS.map((id) => {
const { component: Component, lazy } = SECTION_BY_ID[id];
return (
<StaggeredItem key={id}>
<motion.section
id={id}
variants={itemVariants as any}
className="scroll-mt-24"
>
{lazy ? (
<LazySectionWrapper>
<StaggeredItem key={id}>
<motion.section
id={id}
variants={itemVariants as any}
className="scroll-mt-24"
>
{lazy ? (
<LazySectionWrapper>
<Component />
</LazySectionWrapper>
) : (
<Component />
</LazySectionWrapper>
) : (
<Component />
)}
</motion.section>
</StaggeredItem>
)}
</motion.section>
</StaggeredItem>
);
})}
</StaggeredList>
@@ -901,21 +901,21 @@ export default function GuestDashboard() {
{ACT_6_SECTION_IDS.map((id) => {
const { component: Component, lazy } = SECTION_BY_ID[id];
return (
<StaggeredItem key={id}>
<motion.section
id={id}
variants={itemVariants as any}
className="scroll-mt-24"
>
{lazy ? (
<LazySectionWrapper>
<StaggeredItem key={id}>
<motion.section
id={id}
variants={itemVariants as any}
className="scroll-mt-24"
>
{lazy ? (
<LazySectionWrapper>
<Component />
</LazySectionWrapper>
) : (
<Component />
</LazySectionWrapper>
) : (
<Component />
)}
</motion.section>
</StaggeredItem>
)}
</motion.section>
</StaggeredItem>
);
})}
</StaggeredList>