feat: Revamp Dashboard Story Page with Enhanced Narrative and Milestones

- Introduced a new narrative section detailing the love story, enhancing user engagement.
- Added a timeline feature showcasing significant milestones with rich storytelling elements.
- Improved UI components for better visual appeal, including animations and responsive design.
- Integrated new icons and images to enrich the storytelling experience.
- Updated layout for improved readability and user interaction.
This commit is contained in:
2026-01-26 23:27:37 +02:00
parent 8032a3cee7
commit 68d20046df
2 changed files with 233 additions and 45 deletions

View File

@@ -1,10 +1,13 @@
"use client";
import * as React from "react";
import { useGuest } from "@/context/guest-context";
import { Timeline } from "@/components/features/story/timeline";
import { ArrowLeft, BookOpen } from "lucide-react";
import { ArrowLeft, Heart, Sparkles, MessageCircleHeart, Milestone, Handshake, Hourglass } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { motion } from "framer-motion";
import Image from "next/image";
import { GlassCard } from "@/components/ui/glass-card";
export default function DashboardStoryPage() {
const router = useRouter();
@@ -12,30 +15,180 @@ export default function DashboardStoryPage() {
if (!guest) return null;
const milestones = [
{
date: "2 July 2020",
title: "The First Conversation",
desc: "At 09:39, a simple conversation began. In a season defined by uncertainty and distance, connection found its way quietly—without urgency, but with intention.",
quote: "Some beginnings don't arrive loudly. They simply arrive right.",
icon: MessageCircleHeart,
image: "/ourstory/whatsapp.jpg",
},
{
date: "July 2020 — Through Time",
title: "So Many Firsts",
desc: "From our first date and first holiday, to our first disagreements and the lessons that followed, there were countless firsts. Each one shaped us, deepened us, and quietly proved we were building something real.",
quote: "Through it all, love won.",
icon: Milestone,
},
{
date: "14 November 2025",
title: "The Promise",
desc: "We woke up, and something had shifted. In my own way, I asked the question that mattered most. She said yes, and with that, our intention became forever. A letter was sent to her family, marking the beginning of our next chapter.",
quote: "Love doesn't always ask loudly—sometimes it simply knows.",
icon: Handshake,
},
{
date: "1 March 2026",
title: "The Countdown",
desc: "As this story is being written, the countdown stands at 50 days. Every line of code, every plan, every quiet moment leads us closer to the day our forever begins.",
quote: "Not so long now, just enough time to fall in love all over again.",
icon: Hourglass,
},
];
return (
<div className="space-y-6 max-w-4xl mx-auto pb-20">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/dashboard" className="p-2 hover:bg-neutral-100 rounded-full transition-colors font-body">
<ArrowLeft className="w-5 h-5 text-wedding-ink" />
</Link>
<h1 className="font-serif text-3xl text-wedding-evergreen">Our Story</h1>
<div className="space-y-8 max-w-4xl mx-auto pb-20 px-4">
{/* Header */}
<div className="flex items-center gap-4 pt-6">
<Link href="/dashboard" className="p-2 hover:bg-neutral-100 rounded-full transition-colors font-body">
<ArrowLeft className="w-5 h-5 text-wedding-ink" />
</Link>
<div className="flex items-center gap-3">
<Sparkles className="w-6 h-6 text-wedding-evergreen" />
<h1 className="font-heading text-3xl sm:text-4xl text-wedding-evergreen">Our Story</h1>
</div>
</div>
<div className="max-w-2xl mx-auto space-y-12">
<div className="bg-wedding-sage/10 p-4 rounded-lg flex gap-4 items-start border border-wedding-sage/20">
<BookOpen className="w-6 h-6 text-wedding-sage mt-1" />
<div>
<h3 className="font-medium text-wedding-evergreen">The Journey of Lerato & Denver </h3>
<p className="text-sm text-muted-foreground mt-1">
A look back at the milestones that brought us to this special day. Thank you for being part of our story.
<p className="text-sm text-wedding-moss/60 text-center uppercase tracking-wider">
A journey of love
</p>
{/* Opening Narrative */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, ease: [0.16, 1, 0.3, 1] }}
className="mb-16"
>
<GlassCard className="p-10 md:p-12 shadow-2xl">
<div className="text-center space-y-6">
<div className="inline-flex items-center gap-3 mb-4">
<div className="h-px w-16 bg-gradient-to-r from-transparent to-wedding-evergreen/30" />
<Heart className="h-5 w-5 text-wedding-evergreen/60" fill="currentColor" />
<div className="h-px w-16 bg-gradient-to-l from-transparent to-wedding-evergreen/30" />
</div>
<p className="font-heading italic text-2xl sm:text-3xl text-wedding-evergreen leading-relaxed">
Every great love story begins with a single moment
</p>
<p className="text-base text-wedding-ink/70 leading-relaxed max-w-lg mx-auto">
Our journey began with a gentle suggestion that she might be someone I'd connect with,
in the midst of COVID, when the world grew quieter and time felt different. Through that
season, we found each other in meaningful conversations, laughter, and shared moments,
building a bond that felt calm, grounded, and enduring.
</p>
</div>
</div>
</GlassCard>
</motion.div>
<Timeline />
{/* Timeline with rich storytelling */}
<div className="space-y-12">
{milestones.map((m, i) => (
<motion.div
key={`milestone-${i}-${m.date || m.title}`}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 1, delay: i * 0.2, ease: [0.16, 1, 0.3, 1] }}
className="relative"
>
<GlassCard className="p-8 md:p-10 shadow-2xl hover:shadow-[0_32px_80px_-20px_rgba(52,76,61,0.3)] transition-all duration-700 group">
{/* Decorative timeline connector */}
{i < milestones.length - 1 && (
<div className="absolute left-1/2 -bottom-12 -translate-x-1/2 w-px h-12 bg-gradient-to-b from-wedding-sage/30 via-wedding-moss/20 to-transparent" />
)}
<div className="flex flex-col md:flex-row gap-8 items-start">
{/* Timeline marker */}
<div className="relative flex-shrink-0">
<div className="relative flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-wedding-evergreen/10 to-wedding-sage/10 border-2 border-wedding-evergreen/20 group-hover:border-wedding-evergreen/40 transition-all duration-500">
<motion.div
animate={{ scale: [1, 1.2, 1], opacity: [0.3, 0.5, 0.3] }}
transition={{ duration: 3, repeat: Infinity }}
className="absolute inset-0 bg-wedding-moss rounded-full blur-xl"
/>
<m.icon className="relative h-8 w-8 text-wedding-evergreen" strokeWidth={1.5} />
</div>
</div>
{/* Content */}
<div className="flex-1 space-y-6">
<div>
<div className="text-[10px] font-black uppercase tracking-[0.4em] text-wedding-moss/60 mb-2">
{m.date}
</div>
<h3 className="font-heading text-3xl sm:text-4xl text-wedding-evergreen mb-4">
{m.title}
</h3>
<p className="text-base text-wedding-ink/70 leading-relaxed mb-4">
{m.desc}
</p>
</div>
{/* WhatsApp Screenshot - Only for first conversation */}
{m.image && i === 0 && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.3 }}
className="relative rounded-2xl overflow-hidden border border-white/20 shadow-xl"
>
<Image
src={m.image}
alt="First WhatsApp conversation"
width={600}
height={800}
className="w-full h-auto object-contain"
unoptimized
/>
</motion.div>
)}
{/* Quote card */}
<div className="relative pl-6 border-l-2 border-wedding-sage/30">
<div className="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-wedding-evergreen/40 to-wedding-sage/40" />
<p className="font-heading italic text-xl md:text-2xl text-wedding-evergreen leading-relaxed">
"{m.quote}"
</p>
</div>
</div>
</div>
</GlassCard>
</motion.div>
))}
</div>
{/* Closing Story Note */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 1, delay: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="mt-20"
>
<GlassCard className="p-10 md:p-12 shadow-2xl bg-gradient-to-br from-wedding-sage/5 to-wedding-evergreen/5">
<div className="text-center space-y-4">
<p className="font-heading italic text-2xl sm:text-3xl text-wedding-evergreen">
And now, we invite you to be part of our next chapter
</p>
<p className="text-base text-wedding-ink/70 leading-relaxed max-w-md mx-auto">
Join us as we celebrate our love and begin our forever together.
Your presence will make our special day complete.
</p>
</div>
</GlassCard>
</motion.div>
</div>
);
}

View File

@@ -29,6 +29,8 @@ import {
BookOpen,
Clock,
CheckCircle2,
LayoutGrid,
type LucideIcon,
} from "lucide-react";
// Feature Components - Lazy loaded for performance
@@ -98,6 +100,7 @@ const itemVariants = {
const DASHBOARD_SECTIONS = [
// Act I - Seating (gated)
{ id: "seating", component: LazyTableReveal, label: "Seating", lazy: true },
{ id: "gift", component: WishingWell, label: "Gifts", lazy: false },
// Act 2 - The Plan
{ id: "itinerary", component: LazyItineraryTimeline, label: "Schedule", lazy: true },
{ id: "venue", component: LazyVenueMap, label: "Location", lazy: true },
@@ -115,7 +118,6 @@ const DASHBOARD_SECTIONS = [
{ id: "social", component: SocialMedia, label: "Social", lazy: false },
// Act 6 - Extras
{ id: "directory", component: GuestDirectory, label: "Directory", lazy: false },
{ id: "gift", component: WishingWell, label: "Gifts", lazy: false },
] as const;
type SectionId = (typeof DASHBOARD_SECTIONS)[number]["id"];
@@ -130,7 +132,7 @@ const ACT_2_SECTION_IDS = ["itinerary", "venue", "weather"] as const satisfies r
const ACT_3_SECTION_IDS = ["stay", "attire"] as const satisfies readonly SectionId[];
const ACT_4_SECTION_IDS = ["party", "music"] as const satisfies readonly SectionId[];
const ACT_5_SECTION_IDS = ["guestbook", "gallery", "tribute", "social"] as const satisfies readonly SectionId[];
const ACT_6_SECTION_IDS = ["directory", "gift"] as const satisfies readonly SectionId[];
const ACT_6_SECTION_IDS = ["directory"] as const satisfies readonly SectionId[];
/** =========================
* Chapter Header Component
@@ -754,6 +756,15 @@ export default function GuestDashboard() {
)}
</AnimatePresence>
{/* Gifts - Show right after seating for easy access to gift registry */}
<motion.section
variants={itemVariants as any}
id="gift"
className="scroll-mt-24"
>
<WishingWell />
</motion.section>
{/* Act 2 - The Plan */}
<div key="act-2" className="space-y-8">
<ChapterHeader
@@ -1272,6 +1283,26 @@ const RSVPStatusCard = ({
);
};
/** =========================
* Section ID to Icon Mapping
* ========================= */
const SECTION_ICON_MAP: Record<SectionId, LucideIcon> = {
seating: LayoutGrid, // Changed from Ticket to LayoutGrid to avoid duplicate ticket icons
gift: Heart,
itinerary: Calendar,
venue: MapPin,
weather: Cloud,
stay: BedDouble,
attire: Shirt,
party: Users,
music: MusicIcon,
guestbook: BookOpen,
gallery: ImageIcon,
tribute: Flame,
social: Share2,
directory: UserRound,
};
/** =========================
* Floating Navigation
* ========================= */
@@ -1289,32 +1320,36 @@ const DashboardFloatingNav = ({
const router = useRouter();
const [isConciergeOpen, setIsConciergeOpen] = React.useState(false);
const navItems: FloatingNavItem[] = React.useMemo(
() => [
const navItems: FloatingNavItem[] = React.useMemo(() => {
// Start with special items that aren't in DASHBOARD_SECTIONS
const items: FloatingNavItem[] = [
{ id: "home", label: "Home", icon: Home },
...(showTicket ? [{ id: "ticket", label: "Pass", icon: Ticket } as const] : []),
...(showSeating ? [{ id: "seating", label: "Seating", icon: Ticket } as const] : []),
// Act 2 - The Plan
{ id: "itinerary", label: "Schedule", icon: Calendar },
{ id: "venue", label: "Venue", icon: MapPin },
{ id: "weather", label: "Weather", icon: Cloud },
// Act 3 - Comfort & Style
{ id: "stay", label: "Stay", icon: BedDouble },
{ id: "attire", label: "Attire", icon: Shirt },
// Act 4 - People & Vibe
{ id: "party", label: "Party", icon: Users },
{ id: "music", label: "Music", icon: MusicIcon },
// Act 5 - Participation & Memories
{ id: "guestbook", label: "Guestbook", icon: BookOpen },
{ id: "gallery", label: "Gallery", icon: ImageIcon },
{ id: "tribute", label: "Tributes", icon: Flame },
{ id: "social", label: "Social", icon: Share2 },
// Act 6 - Extras
{ id: "directory", label: "Directory", icon: UserRound },
{ id: "gift", label: "Gifts", icon: Heart },
],
[showSeating, showTicket]
);
];
// Add ticket if shown
if (showTicket) {
items.push({ id: "ticket", label: "Pass", icon: Ticket });
}
// Generate nav items from DASHBOARD_SECTIONS in order
DASHBOARD_SECTIONS.forEach((section) => {
// Filter seating based on showSeating flag
if (section.id === "seating" && !showSeating) {
return;
}
const icon = SECTION_ICON_MAP[section.id];
if (icon) {
items.push({
id: section.id,
label: section.label,
icon,
});
}
});
return items;
}, [showSeating, showTicket]);
return (
<>