Enhance invitation page with theme-based design and features

This update integrates theme-based styling and assets for the invitation page, including dynamic colors, fonts, and images. Added features include improved loading states, detailed event information, RSVP handling, gift registry, and interactive map functionality. This refactor enhances the user experience and supports event-specific personalization.
This commit is contained in:
2025-03-14 05:03:15 +01:00
parent 3862fdb1ad
commit 382df4f83f

View File

@@ -1,181 +1,425 @@
"use client";
import React, { useEffect, useState } from "react";
import { motion } from "framer-motion";
import { useParams } from "next/navigation";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Calendar, Gift, MapPin, Clock } from "lucide-react";
import { useEvents } from "@/context/event-context";
import { useEventThemes } from "@/context/event-theme-context";
import { format, parseISO } from "date-fns";
import Image from "next/image";
import Link from "next/link";
import { useEvents } from "@/context/event-context";
import { EventResponse } from "@/client/types.gen";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Loader2 } from "lucide-react";
import { useParams } from "next/navigation";
import { EventResponse } from "@/client";
import { getServerFileUrl } from "@/lib/utils";
interface InvitationParams {
slug: string;
}
const InvitationPage = () => {
const { slug } = useParams();
const { getEventBySlug } = useEvents();
const { slug } = useParams<{ slug: string }>();
const { event, fetchEventBySlug, isLoadingEvent, eventError } = useEvents();
const [isLoading, setIsLoading] = useState(true);
const [event, setEvent] = useState<EventResponse | null>(null);
const [notFound, setNotFound] = useState(false);
const [showRSVP, setShowRSVP] = useState(false);
const { themes, isLoadingThemes } = useEventThemes();
// Fetch event data when slug is available
useEffect(() => {
const fetchEvent = async () => {
setIsLoading(true);
try {
const eventData = await getEventBySlug(slug as string);
if (eventData) {
setEvent(eventData);
} else {
setNotFound(true);
}
} catch (error) {
console.error("Failed to fetch event data:", error);
setNotFound(true);
} finally {
setIsLoading(false);
}
};
fetchEventBySlug(slug);
}, [slug, fetchEventBySlug]);
if (slug) {
fetchEvent();
}
}, [slug, getEventBySlug]);
const formatDate = (date: string | Date) => {
return new Date(date).toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
};
const formatTime = (date: string | Date) => {
return new Date(date).toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
};
if (isLoading) {
// If loading, show a spinner
if (isLoadingEvent || isLoadingThemes) {
return (
<div className="h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-bounce mb-4">
<Image
src="/api/placeholder/150/150"
alt="Loading"
width={150}
height={150}
className="rounded-full"
/>
</div>
<p className="text-xl font-medium">Loading your invitation...</p>
</div>
<div className="flex h-screen items-center justify-center">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
</div>
);
}
if (notFound || !event) {
// If no event found, show not found message
if (!event) {
return (
<div className="h-screen flex flex-col items-center justify-center text-center space-y-4">
<Image
src="/api/placeholder/404/404"
alt="Not Found"
width={200}
height={200}
className="rounded-xl"
/>
<h2 className="text-3xl font-semibold text-gray-800">
Event Not Found
</h2>
<p className="text-gray-500">
We couldn't find an event matching your invitation. Please check your
invitation link or contact the event organizer.
<div className="flex h-screen flex-col items-center justify-center gap-4 p-4 text-center">
<h1 className="text-3xl font-bold">Invitation Not Found</h1>
<p>
The invitation you're looking for doesn't exist or has been removed.
</p>
<Button asChild>
<Link href="/">Go Back Home</Link>
</Button>
</div>
);
}
// Find the theme for this event
const eventTheme = themes?.find((theme) => theme.id === event.theme_id);
// Default colors if theme is not available
const colors = eventTheme?.color_palette || {
primary: "#90B77D", // Soft jungle green
secondary: "#D2AB67", // Warm giraffe yellow
accent: "#B5A9EA", // Soft hippo purple
accent2: "#8FBDD3", // Elephant blue
accent3: "#E8B87D", // Lion tan
background: "#F9F5F0", // Cream paper texture
text: "#5B4B49", // Warm dark brown
textLight: "#7D6D6B", // Lighter text variant
};
// Format date and time for display
const eventDate = event.event_date
? format(parseISO(event.event_date), "EEEE, MMMM d, yyyy")
: null;
const startTime = event.event_start_time
? format(parseISO(`2000-01-01T${event.event_start_time}`), "HH:mm")
: null;
const endTime = event.event_end_time
? format(parseISO(`2000-01-01T${event.event_end_time}`), "HH:mm")
: null;
const timeRange = startTime && endTime ? `${startTime} - ${endTime}` : null;
// Format RSVP deadline
const rsvpDeadline = event.rsvp_deadline
? format(parseISO(event.rsvp_deadline), "MMMM d, yyyy")
: null;
// Get asset URLs
const getAssetUrl = (key: string) => {
if (eventTheme?.asset_image_urls && eventTheme.asset_image_urls[key]) {
return getServerFileUrl(eventTheme.asset_image_urls[key]);
}
return null;
};
// Background style
const pageStyle = {
backgroundColor: colors.background,
color: colors.text,
fontFamily: eventTheme?.fonts?.body || "sans-serif",
};
const headingStyle = {
fontFamily: eventTheme?.fonts?.heading || "sans-serif",
color: colors.text,
};
return (
<div
className="min-h-screen py-8 px-4 bg-gradient-to-b from-blue-50 to-green-50"
style={{
backgroundImage: "url('/api/placeholder/20/20')",
backgroundRepeat: "repeat",
backgroundSize: "20px 20px",
}}
>
<div className="max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-6"
<div className="min-h-screen pb-10" style={pageStyle}>
<div className="mx-auto max-w-4xl px-4 py-6">
{/* Wooden Sign Title */}
<div
className="mx-auto mb-10 mt-6 rounded-lg p-6 text-center"
style={{
backgroundColor: colors.secondary,
color: colors.text,
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
}}
>
<h1
className="text-4xl sm:text-5xl md:text-6xl font-bold text-emerald-700 mb-2"
style={{ fontFamily: "var(--font-bubblegum)" }}
>
<h1 className="text-4xl font-bold md:text-5xl" style={headingStyle}>
{event.title}
</h1>
<p className="text-xl sm:text-2xl text-amber-600 font-medium">
{event.description}
</p>
</motion.div>
<h2 className="mt-2 text-xl md:text-2xl" style={headingStyle}>
Safari Adventure
</h2>
</div>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="rounded-2xl overflow-hidden shadow-xl bg-white p-4"
>
<Card className="shadow-none border-none">
<CardContent>
<ul className="space-y-4">
<li className="flex items-center gap-2">
<Calendar className="text-green-600" />
<span>{formatDate(event.event_date)}</span>
</li>
<li className="flex items-center gap-2">
<Clock className="text-green-600" />
<span>
{formatTime(event.event_date)} -{" "}
{formatTime(event.event_end_time || "")}
</span>
</li>
<li className="flex items-center gap-2">
<MapPin className="text-green-600" />
<span>
{event.location_name}, {event.location_address}
</span>
</li>
{event.gift_registry_enabled && (
<li className="flex items-center gap-2">
<Gift className="text-green-600" />
<Link href="/gift-registry">
<span className="underline cursor-pointer">
Gift Registry
</span>
</Link>
</li>
)}
</ul>
<Button className="mt-6" onClick={() => setShowRSVP(true)}>
RSVP
</Button>
{showRSVP && (
<div className="mt-4 text-center">
<p>RSVP functionality coming soon!</p>
{/* Safari Animals Banner */}
<div className="mb-10 flex justify-center">
{eventTheme?.foreground_image_url ? (
<div className="relative h-48 w-full max-w-2xl md:h-64">
<Image
src={getServerFileUrl(eventTheme.foreground_image_url) || ""}
alt="Safari Animals"
fill
className="object-contain"
priority
/>
</div>
) : (
<div
className="flex h-48 w-full max-w-2xl items-center justify-center rounded-lg border-2 md:h-64"
style={{
borderColor: colors.primary,
backgroundColor: "rgba(255, 255, 255, 0.5)",
}}
>
<p className="text-center text-lg">Safari Animals Illustration</p>
</div>
)}
</div>
{/* Date/Time with Elephant */}
<div className="mb-10 flex flex-col items-center md:flex-row md:items-start md:space-x-6">
<div className="mb-4 md:mb-0">
<div
className="flex h-28 w-28 items-center justify-center rounded-full border-2 md:h-32 md:w-32"
style={{ borderColor: colors.accent2 }}
>
{getAssetUrl("elephant") ? (
<div className="relative h-24 w-24 md:h-28 md:w-28">
<Image
src={getAssetUrl("elephant") || ""}
alt="Elephant"
fill
className="object-contain"
/>
</div>
) : (
<p className="text-center text-sm">Elephant</p>
)}
</CardContent>
</div>
</div>
<Card
className="flex-1 p-6"
style={{
backgroundColor: "rgba(240, 233, 214, 0.7)",
borderColor: colors.accent2,
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)",
}}
>
<h3
className="mb-2 text-center text-2xl font-bold md:text-left"
style={headingStyle}
>
{eventDate}
</h3>
{timeRange && (
<p
className="text-center text-lg md:text-left"
style={{ color: colors.text }}
>
{timeRange}
</p>
)}
</Card>
</motion.div>
</div>
{/* Location with Giraffe */}
<div className="mb-10 flex flex-col-reverse items-center md:flex-row md:items-start md:space-x-6">
<Card
className="flex-1 p-6"
style={{
backgroundColor: "rgba(240, 233, 214, 0.7)",
borderColor: colors.secondary,
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)",
}}
>
<h3
className="mb-2 text-center text-2xl font-bold md:text-left"
style={headingStyle}
>
{event.location_name}
</h3>
<p
style={{ color: colors.text }}
className="text-center md:text-left"
>
{event.location_address}
</p>
{event.location_url && (
<div className="mt-4 text-center md:text-left">
<Link
href={event.location_url}
target="_blank"
rel="noopener noreferrer"
>
<Button
className="rounded-full"
style={{
backgroundColor: colors.secondary,
color: "white",
}}
>
View Map
</Button>
</Link>
</div>
)}
</Card>
<div className="mb-4 md:mb-0">
<div
className="flex h-28 w-28 items-center justify-center rounded-full border-2 md:h-32 md:w-32"
style={{ borderColor: colors.secondary }}
>
{getAssetUrl("giraffe") ? (
<div className="relative h-24 w-24 md:h-28 md:w-28">
<Image
src={getAssetUrl("giraffe") || ""}
alt="Giraffe"
fill
className="object-contain"
/>
</div>
) : (
<p className="text-center text-sm">Giraffe</p>
)}
</div>
</div>
</div>
{/* Description */}
<Card
className="mb-10 border-2 p-6"
style={{
backgroundColor: "white",
borderColor: colors.primary,
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.05)",
}}
>
<h3
className="mb-4 text-center text-2xl font-bold"
style={headingStyle}
>
La nostra piccola safari adventure!
</h3>
<div
className="prose mx-auto max-w-none"
style={{ color: colors.text }}
>
{event.description ? (
<div className="whitespace-pre-line text-center">
{event.description}
</div>
) : (
<p className="text-center">
Join us for a wild safari celebration with games, food, and
jungle fun! Safari attire encouraged but optional. Children
welcome to dress as their favorite animals for a truly wild
experience!
</p>
)}
</div>
</Card>
{/* RSVP Section */}
{event.rsvp_enabled && (
<div
className="mb-10 rounded-xl p-6"
style={{
backgroundColor: colors.accent,
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
}}
>
<h3
className="mb-2 text-center text-2xl font-bold text-white"
style={headingStyle}
>
RSVP
</h3>
<p className="mb-4 text-center text-white">
{rsvpDeadline
? `Please respond by ${rsvpDeadline}`
: "Please respond as soon as possible"}
</p>
<div className="flex justify-center">
<Link href={`/rsvp/${event.slug}`}>
<Button
className="rounded-full px-8 py-6 text-lg"
style={{
backgroundColor: "white",
color: colors.text,
}}
>
Respond Now
</Button>
</Link>
</div>
</div>
)}
{/* Gift Registry with Lion */}
{event.gift_registry_enabled && (
<div className="mb-10 flex flex-col items-center md:flex-row md:items-center md:space-x-6">
<div>
<div
className="flex h-24 w-24 items-center justify-center rounded-full border-2 md:h-28 md:w-28"
style={{ borderColor: colors.accent3 }}
>
{getAssetUrl("lion") ? (
<div className="relative h-20 w-20 md:h-24 md:w-24">
<Image
src={getAssetUrl("lion") || ""}
alt="Lion"
fill
className="object-contain"
/>
</div>
) : (
<p className="text-center text-sm">Lion</p>
)}
</div>
</div>
<Card
className="mt-4 flex-1 p-6 md:mt-0"
style={{
backgroundColor: "rgba(240, 233, 214, 0.7)",
borderColor: colors.accent3,
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)",
}}
>
<h3
className="mb-2 text-center text-2xl font-bold md:text-left"
style={headingStyle}
>
Gift Registry
</h3>
<p
style={{ color: colors.text }}
className="text-center md:text-left"
>
View Emma's Wishes
</p>
<div className="mt-4 text-center md:text-left">
<Link href={`/gifts/${event.slug}`}>
<Button
className="rounded-full"
style={{
backgroundColor: colors.accent3,
color: "white",
}}
>
View Gifts
</Button>
</Link>
</div>
</Card>
</div>
)}
{/* View Map Button */}
{event.location_url && (
<div className="mb-10 flex justify-center">
<Link
href={event.location_url}
target="_blank"
rel="noopener noreferrer"
>
<Button
className="rounded-full px-8 py-6 text-lg"
style={{
backgroundColor: colors.primary,
color: "white",
}}
>
View Location Map
</Button>
</Link>
</div>
)}
{/* Footer with Contact Info */}
<div
className="mx-auto max-w-2xl rounded-lg p-6 text-center"
style={{
backgroundColor: "rgba(240, 233, 214, 0.5)",
borderColor: colors.secondary,
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)",
}}
>
<p className="mb-2 font-semibold">Contact:</p>
{event.contact_email && <p className="mb-1">{event.contact_email}</p>}
{event.contact_phone && <p>{event.contact_phone}</p>}
</div>
</div>
</div>
);