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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user