import { redirect } from "next/navigation" import { getInvitationInteractions, replaceInvitationInteractions, } from "@/lib/db/queries/interactions" import { getInvitationByToken, submitInvitationResponse } from "@/lib/db/queries/invitations" import { getPhotoUploadByInvitationId } from "@/lib/db/queries/uploads" import { getGuestDictionary } from "@/lib/i18n/get-dictionary" import { getRsvpDeadline, isRsvpClosed } from "@/lib/rsvp-deadline" import { PhotoUpload } from "@/components/invite/photo-upload" import { interactionSubmissionSchema, } from "@/lib/validation/interactions" import { mealPreferenceSchema, submitInvitationResponseSchema, type MealPreference, } from "@/lib/validation/response" export const dynamic = "force-dynamic" const mealOptions: MealPreference[] = ["meat", "fish", "vegetarian", "vegan", "kids", "other"] function formatDateForInput(value: Date | string | null | undefined) { if (!value) return "" if (value instanceof Date) { return value.toISOString().slice(0, 10) } return value } function getOptionalString(formData: FormData, key: string) { return formData.get(key)?.toString() ?? "" } function getTransportHelp(value: FormDataEntryValue | null) { if (value === "yes") return true if (value === "no") return false return null } function getToastRequestFromFormData(formData: FormData) { if (!formData.has("wantsToast")) return null const speakerName = getOptionalString(formData, "toastSpeakerName") if (!speakerName) return null const memberId = getOptionalString(formData, "toastMemberId") return { memberId: memberId || null, speakerName, estimatedMinutes: getOptionalString(formData, "toastEstimatedMinutes"), } } function deadlineMessage(template: string) { const deadline = getRsvpDeadline() return template.replace("{date}", deadline ?? "the deadline") } async function submitRsvpAction(token: string, formData: FormData) { "use server" if (isRsvpClosed()) { redirect(`/invite/${token}?error=closed`) } const invitation = await getInvitationByToken(token) if (!invitation) { redirect(`/invite/${token}?error=invalid`) } const members = invitation.members.map((member) => { const attendingValue = formData.get(`attending-${member.id}`) if (attendingValue !== "yes" && attendingValue !== "no") { redirect(`/invite/${token}?error=generic`) } const attending = attendingValue === "yes" const rawMealPreference = formData.get(`meal-${member.id}`)?.toString() ?? "" const mealPreference = mealPreferenceSchema.safeParse(rawMealPreference) return { memberId: member.id, attending, mealPreference: attending && mealPreference.success ? mealPreference.data : null, dietaryRestrictions: getOptionalString(formData, `dietary-${member.id}`), } }) const parsed = submitInvitationResponseSchema.safeParse({ token, members, arrivalDate: formData.get("arrivalDate"), departureDate: formData.get("departureDate"), stayLocation: formData.get("stayLocation"), needsTransportHelp: getTransportHelp(formData.get("needsTransportHelp")), messageToCouple: formData.get("messageToCouple"), }) if (!parsed.success) { redirect(`/invite/${token}?error=generic`) } const interactions = interactionSubmissionSchema.safeParse({ invitationId: invitation.invitation.id, toastRequest: getToastRequestFromFormData(formData), }) if (!interactions.success) { redirect(`/invite/${token}?error=generic`) } await submitInvitationResponse(parsed.data) await replaceInvitationInteractions(interactions.data) redirect(`/invite/${token}?submitted=1`) } export default async function InvitePage({ params, searchParams, }: { params: Promise<{ token: string }> searchParams: Promise<{ submitted?: string; error?: string }> }) { const [{ token }, { submitted, error }] = await Promise.all([params, searchParams]) const detail = await getInvitationByToken(token) if (!detail) { const dictionary = getGuestDictionary("en") return (

{dictionary.hero.eyebrow}

{dictionary.invalid.title}

{dictionary.invalid.description}

) } const dictionary = getGuestDictionary(detail.invitation.language) const [interactions, photoUpload] = await Promise.all([ getInvitationInteractions(detail.invitation.id), getPhotoUploadByInvitationId(detail.invitation.id), ]) const closed = isRsvpClosed() const action = submitRsvpAction.bind(null, token) const greeting = dictionary.hero.greeting.replace("{name}", detail.invitation.display_name) const toastRequest = interactions.toastRequests[0] return (

{dictionary.hero.eyebrow}

{dictionary.hero.title}

{greeting}

{dictionary.hero.description}

{submitted ? (
{dictionary.form.submitted}
) : null} {error ? (
{error === "closed" ? dictionary.errors.closed : dictionary.errors.generic}
) : null}

{dictionary.wedding.dateLabel}

{dictionary.wedding.dateValue}

{dictionary.wedding.locationLabel}

{dictionary.wedding.locationValue}

{dictionary.wedding.accommodationLabel}

{dictionary.wedding.accommodationValue}

{dictionary.sections.rsvp}

{closed ? dictionary.deadline.closed : deadlineMessage(dictionary.deadline.open)}

{detail.members.map((member) => (
{member.name}

{dictionary.form.attending}

))}

{dictionary.sections.travel}

{dictionary.form.needsTransportHelp}