import Link from "next/link" import { notFound } from "next/navigation" import { CopyInviteButton } from "@/components/admin/copy-invite-button" import { DeleteInvitationDialog } from "@/components/admin/delete-invitation-dialog" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { getInvitationInteractions, updateToastRequestStatus, } from "@/lib/db/queries/interactions" import { getInvitationDetail } from "@/lib/db/queries/invitations" import { getPhotoUploadByInvitationId } from "@/lib/db/queries/uploads" import { getLanguageLabel } from "@/lib/i18n/config" import { createPresignedDownloadUrl } from "@/lib/storage/uploads" import { toastStatusSchema } from "@/lib/validation/interactions" export const dynamic = "force-dynamic" function getInviteUrl(token: string) { return new URL(`/invite/${token}`, process.env.APP_URL ?? "http://localhost:3000").toString() } function formatDate(date: Date | null | undefined) { if (!date) return "—" return new Intl.DateTimeFormat("en", { dateStyle: "medium", timeStyle: "short", }).format(date) } function formatBoolean(value: boolean | null) { if (value === true) return "Yes" if (value === false) return "No" return "Pending" } function formatDateOnly(value: Date | string | null | undefined) { if (!value) return "—" if (value instanceof Date) { return new Intl.DateTimeFormat("en", { dateStyle: "medium" }).format(value) } return value } async function updateToastStatusAction(formData: FormData) { "use server" const toastId = formData.get("toastId")?.toString() const status = toastStatusSchema.safeParse(formData.get("status")) if (!toastId || !status.success) return await updateToastRequestStatus(toastId, status.data) } export default async function GuestDetailPage({ params, searchParams, }: { params: Promise<{ id: string }> searchParams: Promise<{ created?: string }> }) { const [{ id }, { created }] = await Promise.all([params, searchParams]) const [detail, interactions, photoUpload] = await Promise.all([ getInvitationDetail(id), getInvitationInteractions(id), getPhotoUploadByInvitationId(id), ]) if (!detail) notFound() const inviteUrl = getInviteUrl(detail.invitation.token) const photoUrl = photoUpload ? await createPresignedDownloadUrl(photoUpload.object_key) : null return (
{detail.derivedStatus} {getLanguageLabel(detail.invitation.language)}

{detail.invitation.display_name}

Invitation details and member list.

{created ? ( Invitation created Copy the private invite link below and share it only with this invitation group. ) : null}
Invite link Private URL for this invitation group. {inviteUrl} Metadata Admin-managed invitation fields.
Contact email {detail.invitation.contact_email ?? "—"}
Contact phone {detail.invitation.contact_phone ?? "—"}
Max party size {detail.invitation.max_party_size}
Member edits {detail.invitation.allow_member_edits ? "Allowed" : "Disabled"}
Updated {formatDate(detail.invitation.updated_at)}
Members Individual attendance and meal details from the guest RSVP. Name Primary Attending Meal Dietary restrictions {detail.members.map((member) => ( {member.name} {member.is_primary_contact ? "Yes" : "No"} {formatBoolean(member.attending)} {member.meal_preference ?? "—"} {member.dietary_restrictions ?? "—"} ))}
Travel and RSVP details Group-level logistics submitted by the invitation group. {detail.response ? (

Submitted

{formatDate(detail.response.submitted_at)}

Needs transport help

{formatBoolean(detail.response.needs_transport_help)}

Arrival date

{formatDateOnly(detail.response.arrival_date)}

Departure date

{formatDateOnly(detail.response.departure_date)}

Staying at

{detail.response.stay_location ?? "—"}

Message to the couple

{detail.response.message_to_couple ?? "—"}

) : (

No RSVP response submitted yet.

)}
Admin notes

{detail.invitation.notes ?? "No notes yet."}

Wedding surprise photo One image uploaded by this invitation group. {photoUpload && photoUrl ? (
{/* eslint-disable-next-line @next/next/no-img-element */} {photoUpload.original_filename

Filename

{photoUpload.original_filename ?? "—"}

Content type

{photoUpload.content_type ?? "—"}

Size

{photoUpload.size_bytes ? `${(photoUpload.size_bytes / 1024 / 1024).toFixed(2)} MB` : "—"}

) : (

No photo uploaded yet.

)}
Toast requests Optional speech requests. Status is managed by admins. {interactions.toastRequests.length > 0 ? (
{interactions.toastRequests.map((toast) => (

{toast.speaker_name}

{toast.status}

Estimated time: {toast.estimated_minutes ?? "—"} minutes

{(["pending", "accepted", "declined", "scheduled"] as const).map( (status) => (
), )}
))}
) : (

No toast requests submitted yet.

)}
) }