refactor: migrate dashboard pages from (auth) to (dashboard) route group and update dashboard view logic.
This commit is contained in:
@@ -42,6 +42,8 @@ const nextConfig: NextConfig = {
|
||||
root: path.join(__dirname), // Use current directory (app/) as root
|
||||
},
|
||||
|
||||
serverExternalPackages: ['pg', '@prisma/client', 'prisma', '@prisma/adapter-pg'],
|
||||
|
||||
// Security headers for additional protection
|
||||
async headers() {
|
||||
return [
|
||||
|
||||
@@ -1,56 +1,71 @@
|
||||
import { PrismaClient, RsvpStatus } from '@prisma/client';
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { Pool } from "pg";
|
||||
import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
|
||||
// Set fallback environment variable before instantiating client
|
||||
process.env.DATABASE_URL = process.env.DATABASE_URL || "postgresql://postgres:postgres@127.0.0.1:54322/postgres";
|
||||
// Load env vars
|
||||
dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
const pool = new Pool({ connectionString });
|
||||
const adapter = new PrismaPg(pool);
|
||||
|
||||
// Initialize Prisma
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
async function main() {
|
||||
console.log('Starting RSVP status migration...');
|
||||
const guests = await prisma.guest.findMany();
|
||||
console.log(`Found ${guests.length} guests to migrate.`);
|
||||
console.log("Starting migration of RSVP status...");
|
||||
|
||||
let updatedCount = 0;
|
||||
try {
|
||||
// 1. Get all guests
|
||||
const guests = await prisma.guest.findMany();
|
||||
console.log(`Found ${guests.length} guests to check.`);
|
||||
|
||||
for (const guest of guests) {
|
||||
let status: RsvpStatus = RsvpStatus.PENDING;
|
||||
let updatedCount = 0;
|
||||
|
||||
// Map legacy isAttending boolean to RsvpStatus enum
|
||||
if (guest.isAttending === true) {
|
||||
status = RsvpStatus.ACCEPTED;
|
||||
} else if (guest.isAttending === false) {
|
||||
status = RsvpStatus.DECLINED;
|
||||
}
|
||||
for (const guest of guests) {
|
||||
// Skip if already migrated
|
||||
if (guest.rsvpStatus !== 'PENDING') continue;
|
||||
|
||||
// Determine profile completion status
|
||||
const hasDietary = !!guest.dietaryRestrictions;
|
||||
// Check if relationships are set (basic heuristic)
|
||||
const hasRelationship = !!guest.relationship && guest.relationship !== 'Guest';
|
||||
let newStatus: 'ACCEPTED' | 'DECLINED' | 'PENDING' = 'PENDING';
|
||||
let completedAt = null;
|
||||
|
||||
// Simple logic: if they accepted and have some details, mark profile partially complete?
|
||||
// Actually, let's leave profileCompleted as false to prompt them to use the new dashboard,
|
||||
// UNLESS they have filled out a lot of data.
|
||||
// For now, let's just migrate the RSVP status which is critical.
|
||||
// Legacy `isAttending` was boolean or null
|
||||
// If isAttending is true -> ACCEPTED
|
||||
// If isAttending is false -> DECLINED
|
||||
// If null -> PENDING
|
||||
|
||||
await prisma.guest.update({
|
||||
where: { id: guest.id },
|
||||
data: {
|
||||
rsvpStatus: status,
|
||||
rsvpCompletedAt: guest.rsvpTimestamp || (status !== RsvpStatus.PENDING ? new Date() : null), // Use existing timestamp or now if missing but replied
|
||||
// Note: Prisma returns boolean true/false or null
|
||||
if ((guest as any).isAttending === true) {
|
||||
newStatus = 'ACCEPTED';
|
||||
completedAt = (guest as any).rsvpTimestamp || new Date();
|
||||
} else if ((guest as any).isAttending === false) {
|
||||
newStatus = 'DECLINED';
|
||||
completedAt = (guest as any).rsvpTimestamp || new Date();
|
||||
}
|
||||
});
|
||||
updatedCount++;
|
||||
}
|
||||
|
||||
console.log(`Migration complete. Updated ${updatedCount} guests.`);
|
||||
if (newStatus !== 'PENDING') {
|
||||
await prisma.guest.update({
|
||||
where: { id: guest.id },
|
||||
data: {
|
||||
rsvpStatus: newStatus,
|
||||
rsvpCompletedAt: completedAt,
|
||||
// also set legacy field for consistency? it's already set
|
||||
}
|
||||
});
|
||||
updatedCount++;
|
||||
process.stdout.write(`.`);
|
||||
}
|
||||
}
|
||||
console.log(`\nMigration complete. Updated ${updatedCount} guests.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Migration failed:", error);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e)
|
||||
await prisma.$disconnect()
|
||||
process.exit(1)
|
||||
})
|
||||
main();
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
"use client";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const DomeGalleryContainer = lazy(() =>
|
||||
import("@/components/features/gallery/dome-gallery-container").then(
|
||||
(mod) => ({ default: mod.DomeGalleryContainer })
|
||||
)
|
||||
);
|
||||
const GuestUpload = lazy(() =>
|
||||
import("@/components/features/gallery/guest-upload").then((mod) => ({
|
||||
default: mod.GuestUpload,
|
||||
}))
|
||||
);
|
||||
|
||||
export default function GalleryPage() {
|
||||
// simple trick: change a key to force DomeGalleryContainer to re-run its fetch
|
||||
const [refreshKey, setRefreshKey] = React.useState(0);
|
||||
|
||||
const handleUploadComplete = () => {
|
||||
setRefreshKey((k) => k + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 pt-8 px-4 bg-wedding-canvas">
|
||||
{/* Header */}
|
||||
<div className="max-w-md mx-auto mb-6 flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="w-10 h-10 rounded-full bg-wedding-ink/10 dark:bg-wedding-ink/20 flex items-center justify-center hover:bg-wedding-ink/20 dark:hover:bg-wedding-ink/30 transition-colors text-wedding-ink dark:text-wedding-ink"
|
||||
>
|
||||
←
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="font-heading text-3xl text-wedding-ink">
|
||||
Shared Dome Gallery
|
||||
</h1>
|
||||
<p className="text-xs uppercase tracking-widest text-wedding-ink/50">
|
||||
The Moyos · Live wedding moments
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="mx-auto flex w-full max-w-md flex-col items-center gap-6">
|
||||
{/* 3D dome (couple + live guest uploads) */}
|
||||
<section className="w-full h-[420px] rounded-3xl bg-gradient-to-b from-wedding-ink/80 via-wedding-forest/70 to-wedding-ink/90 px-3 py-3 shadow-sage-lg">
|
||||
<Suspense fallback={<Skeleton className="h-full w-full" />}>
|
||||
<DomeGalleryContainer key={refreshKey} />
|
||||
</Suspense>
|
||||
</section>
|
||||
|
||||
{/* Guest upload card */}
|
||||
<Suspense fallback={<Skeleton className="h-64 w-full" />}>
|
||||
<GuestUpload onUploadComplete={handleUploadComplete} />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { GuestbookFeed } from "@/components/features/guestbook/guestbook-feed";
|
||||
import { useGuest } from "@/context/guest-context";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function GuestbookPage() {
|
||||
const { guest } = useGuest();
|
||||
|
||||
// If someone lands here directly without logging in
|
||||
if (!guest)
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-10 text-center bg-wedding-canvas">
|
||||
<div className="text-wedding-ink dark:text-wedding-ink">Please login first.</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-20 pt-8 px-4 bg-wedding-canvas">
|
||||
{/* Custom Header with Back Button */}
|
||||
<div className="max-w-2xl mx-auto mb-8 flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="w-10 h-10 rounded-full bg-wedding-ink/10 dark:bg-wedding-ink/20 flex items-center justify-center hover:bg-wedding-ink/20 dark:hover:bg-wedding-ink/30 transition-colors text-wedding-ink dark:text-wedding-ink"
|
||||
>
|
||||
←
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="font-heading text-3xl text-wedding-ink">Guestbook</h1>
|
||||
<p className="text-xs uppercase tracking-widest text-wedding-ink/50">
|
||||
Messages of Love
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GuestbookFeed />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { lazy, Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const SongRequest = lazy(() =>
|
||||
import("@/components/features/music/song-request").then((mod) => ({
|
||||
default: mod.SongRequest,
|
||||
}))
|
||||
);
|
||||
|
||||
export default function MusicPage() {
|
||||
return (
|
||||
<div className="min-h-screen pb-20 pt-8 px-4 bg-wedding-canvas">
|
||||
<div className="max-w-md mx-auto mb-8 flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="w-10 h-10 rounded-full bg-wedding-ink/10 dark:bg-wedding-ink/20 flex items-center justify-center hover:bg-wedding-ink/20 dark:hover:bg-wedding-ink/30 transition-colors text-wedding-ink dark:text-wedding-ink"
|
||||
>
|
||||
←
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="font-heading text-3xl text-wedding-ink">DJ Requests</h1>
|
||||
<p className="text-xs uppercase tracking-widest text-wedding-ink/50">
|
||||
Set the vibe
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<Skeleton className="h-96 w-full" />}>
|
||||
<SongRequest />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, useState, useEffect } from "react";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { useGuest } from "@/context/guest-context";
|
||||
import GuestDashboard from "@/components/features/dashboard/dashboard-view";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { CheckCircle2, Info } from "lucide-react";
|
||||
|
||||
function DashboardContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const { guest, isLoading } = useGuest();
|
||||
const rsvpStatus = searchParams.get("rsvp");
|
||||
const guestName = searchParams.get("name");
|
||||
|
||||
// Redirect to RSVP if guest hasn't responded
|
||||
useEffect(() => {
|
||||
// Wait for guest data to load
|
||||
if (isLoading) return;
|
||||
|
||||
// If no guest, they need to authenticate first (will be handled by auth guard)
|
||||
if (!guest) return;
|
||||
|
||||
// If guest hasn't RSVP'd, redirect to RSVP form
|
||||
if (!guest.hasResponded && guest.inviteCode) {
|
||||
router.replace(`/rsvp?invite=${guest.inviteCode}`);
|
||||
}
|
||||
}, [guest, isLoading, router]);
|
||||
|
||||
// Show loading state while checking
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="w-16 h-16 rounded-full border-4 border-wedding-sage/20 border-t-wedding-sage animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If guest hasn't responded, don't render dashboard (redirect is in progress)
|
||||
if (guest && !guest.hasResponded) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="w-16 h-16 rounded-full border-4 border-wedding-sage/20 border-t-wedding-sage animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{rsvpStatus === "already-submitted" && guestName && (
|
||||
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 max-w-md w-full mx-4">
|
||||
<Alert className="bg-wedding-sage/10 border-wedding-sage">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
<AlertTitle>Welcome back, {guestName}!</AlertTitle>
|
||||
<AlertDescription>
|
||||
You've already submitted your RSVP. You can view your
|
||||
dashboard below.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
<GuestDashboard />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="w-16 h-16 rounded-full border-4 border-wedding-sage/20 border-t-wedding-sage animate-spin" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DashboardContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { lazy, Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const Timeline = lazy(() =>
|
||||
import("@/components/features/story/timeline").then((mod) => ({
|
||||
default: mod.Timeline,
|
||||
}))
|
||||
);
|
||||
|
||||
export default function StoryPage() {
|
||||
return (
|
||||
<div className="min-h-screen pb-20 pt-8 px-4 bg-wedding-canvas">
|
||||
<div className="max-w-2xl mx-auto mb-8 flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="w-10 h-10 rounded-full bg-wedding-ink/10 dark:bg-wedding-ink/20 flex items-center justify-center hover:bg-wedding-ink/20 dark:hover:bg-wedding-ink/30 transition-colors text-wedding-ink dark:text-wedding-ink"
|
||||
>
|
||||
←
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="font-heading text-3xl text-wedding-ink">Our Story</h1>
|
||||
<p className="text-xs uppercase tracking-widest text-wedding-ink/50">
|
||||
How it started
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<Skeleton className="h-96 w-full" />}>
|
||||
<Timeline />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { lazy, Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import { useGuest } from "@/context/guest-context";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const WeddingPass = lazy(() =>
|
||||
import("@/components/features/ticket/wedding-pass").then((mod) => ({
|
||||
default: mod.WeddingPass,
|
||||
}))
|
||||
);
|
||||
const AddToCalendar = lazy(() =>
|
||||
import("@/components/features/calendar/add-to-calendar").then((mod) => ({
|
||||
default: mod.AddToCalendar,
|
||||
}))
|
||||
);
|
||||
|
||||
export default function TicketPage() {
|
||||
const { guest } = useGuest();
|
||||
|
||||
// Guard clause: If they haven't RSVP'd, kick them out
|
||||
if (!guest?.hasResponded) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center text-center p-6 bg-wedding-canvas">
|
||||
<div>
|
||||
<h1 className="text-wedding-ink dark:text-wedding-ink text-xl mb-4">Access Denied</h1>
|
||||
<p className="text-wedding-ink/60 dark:text-wedding-ink/60 mb-6">
|
||||
Please RSVP to generate your ticket.
|
||||
</p>
|
||||
<Link href="/wedding" className="text-wedding-evergreen dark:text-wedding-sage underline">
|
||||
Back to Info Hub
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-20 pt-8 px-4 bg-wedding-canvas">
|
||||
{/* Navigation Header */}
|
||||
<div className="max-w-md mx-auto mb-10 flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="w-10 h-10 rounded-full bg-wedding-ink/10 dark:bg-wedding-ink/20 flex items-center justify-center hover:bg-wedding-ink/20 dark:hover:bg-wedding-ink/30 transition-colors text-wedding-ink dark:text-wedding-ink"
|
||||
>
|
||||
←
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="font-heading text-3xl text-wedding-ink dark:text-wedding-ink">Your Pass</h1>
|
||||
<p className="text-xs uppercase tracking-widest text-wedding-ink/50 dark:text-wedding-ink/50">
|
||||
Security & Check-in
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
{/* 1. The Ticket Visual + Download Button (Handled inside this component) */}
|
||||
<Suspense fallback={<Skeleton className="h-96 w-full max-w-[340px]" />}>
|
||||
<WeddingPass />
|
||||
</Suspense>
|
||||
|
||||
{/* 2. The Calendar Button (Placed below the ticket) */}
|
||||
<div className="w-full max-w-[340px]">
|
||||
<Suspense fallback={<Skeleton className="h-16 w-full" />}>
|
||||
<AddToCalendar />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { lazy, Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const TributeWall = lazy(() =>
|
||||
import("@/components/features/tribute/tribute-wall").then((mod) => ({
|
||||
default: mod.TributeWall,
|
||||
}))
|
||||
);
|
||||
|
||||
export default function TributePage() {
|
||||
return (
|
||||
<div className="min-h-screen pb-20 pt-8 px-4 bg-wedding-canvas">
|
||||
<div className="max-w-2xl mx-auto mb-8 flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="w-10 h-10 rounded-full bg-wedding-ink/10 dark:bg-wedding-ink/20 flex items-center justify-center hover:bg-wedding-ink/20 dark:hover:bg-wedding-ink/30 transition-colors text-wedding-ink dark:text-wedding-ink"
|
||||
>
|
||||
←
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="font-heading text-3xl text-wedding-ink">Tribute Wall</h1>
|
||||
<p className="text-xs uppercase tracking-widest text-wedding-ink/50">
|
||||
Forever in our hearts
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<Suspense fallback={<Skeleton className="h-96 w-full" />}>
|
||||
<TributeWall />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
src/app/(dashboard)/dashboard/gallery/page.tsx
Normal file
48
src/app/(dashboard)/dashboard/gallery/page.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { useGuest } from "@/context/guest-context";
|
||||
import { DomeGalleryContainer } from "@/components/features/gallery/dome-gallery-container";
|
||||
import { GuestUpload } from "@/components/features/gallery/guest-upload";
|
||||
import { ArrowLeft, Camera } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function DashboardGalleryPage() {
|
||||
const router = useRouter();
|
||||
const { guest } = useGuest();
|
||||
|
||||
if (!guest) return null;
|
||||
|
||||
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">Gallery</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
|
||||
<div className="space-y-6">
|
||||
<div className="bg-wedding-sage/10 p-4 rounded-lg flex gap-4 items-start border border-wedding-sage/20">
|
||||
<Camera className="w-6 h-6 text-wedding-sage mt-1" />
|
||||
<div>
|
||||
<h3 className="font-medium text-wedding-evergreen">Live Photo Dome 📸</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Every photo you and other guests upload appears here in real-time. Share your favorite moments from the celebration!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GuestUpload />
|
||||
</div>
|
||||
|
||||
<div className="lg:h-[calc(100vh-200px)] h-[500px] sticky top-24 rounded-2xl overflow-hidden border shadow-inner bg-wedding-ivory">
|
||||
<DomeGalleryContainer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useGuest } from "@/context/guest-context";
|
||||
import { Loader2, LogOut, LayoutDashboard, User, Users, MessageSquare, Ticket, Music } from "lucide-react";
|
||||
import { Loader2, LogOut, LayoutDashboard, User, Users, MessageSquare, Ticket, Music, Flame, BookOpen, Camera, Info } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -60,7 +60,12 @@ export default function DashboardLayout({
|
||||
<DashboardLink href="/dashboard/profile" icon={User} label="My Profile" />
|
||||
<DashboardLink href="/dashboard/whos-who" icon={Users} label="Who's Who" />
|
||||
<DashboardLink href="/dashboard/guestbook" icon={MessageSquare} label="Guestbook" />
|
||||
<DashboardLink href="/wedding" icon={Ticket} label="Event Info" />
|
||||
<DashboardLink href="/dashboard/gallery" icon={Camera} label="Gallery" />
|
||||
<DashboardLink href="/dashboard/music" icon={Music} label="Music" />
|
||||
<DashboardLink href="/dashboard/story" icon={BookOpen} label="Our Story" />
|
||||
<DashboardLink href="/dashboard/tribute" icon={Flame} label="Tribute Wall" />
|
||||
<DashboardLink href="/dashboard/ticket" icon={Ticket} label="My Pass" />
|
||||
<DashboardLink href="/wedding" icon={Info} label="Event Info" />
|
||||
</nav>
|
||||
|
||||
<div className="pt-6 border-t">
|
||||
@@ -89,9 +94,10 @@ export default function DashboardLayout({
|
||||
{/* Mobile Bottom Nav */}
|
||||
<nav className="md:hidden fixed bottom-0 left-0 right-0 bg-white border-t h-16 flex items-center justify-around px-2 z-50 pb-safe">
|
||||
<MobileNavLink href="/dashboard" icon={LayoutDashboard} label="Home" />
|
||||
<MobileNavLink href="/dashboard/profile" icon={User} label="Profile" />
|
||||
<MobileNavLink href="/dashboard/whos-who" icon={Users} label="Tree" />
|
||||
<MobileNavLink href="/dashboard/guestbook" icon={MessageSquare} label="Note" />
|
||||
<MobileNavLink href="/dashboard/ticket" icon={Ticket} label="Pass" />
|
||||
<MobileNavLink href="/dashboard/gallery" icon={Camera} label="Gallery" />
|
||||
<MobileNavLink href="/dashboard/music" icon={Music} label="Music" />
|
||||
<MobileNavLink href="/dashboard/profile" icon={User} label="Me" />
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
|
||||
49
src/app/(dashboard)/dashboard/music/page.tsx
Normal file
49
src/app/(dashboard)/dashboard/music/page.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useGuest } from "@/context/guest-context";
|
||||
import { CouplesPlaylistDisplay } from "@/components/features/music/couples-playlist-display";
|
||||
import { SongRequest } from "@/components/features/music/song-request";
|
||||
import { ArrowLeft, Music } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function DashboardMusicPage() {
|
||||
const router = useRouter();
|
||||
const { guest } = useGuest();
|
||||
|
||||
if (!guest) return null;
|
||||
|
||||
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">Music</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
|
||||
<div className="space-y-6">
|
||||
<div className="bg-wedding-sage/10 p-4 rounded-lg flex gap-4 items-start border border-wedding-sage/20">
|
||||
<Music className="w-6 h-6 text-wedding-sage mt-1" />
|
||||
<div>
|
||||
<h3 className="font-medium text-wedding-evergreen">Request your favorites 🎵</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Help us set the mood! Search for your favorite tracks and add them to the request list. You can also vote for songs other guests have recommended.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SongRequest />
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<h2 className="font-serif text-2xl text-wedding-evergreen">Our Curated Playlist</h2>
|
||||
<CouplesPlaylistDisplay />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,165 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useGuest } from "@/context/guest-context";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowRight, UserCircle, CheckCircle2, AlertCircle, Camera, Users, MessageSquare } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import GuestDashboard from "@/components/features/dashboard/dashboard-view";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { guest } = useGuest();
|
||||
if (!guest) return null;
|
||||
|
||||
// Calculate completion
|
||||
const checks = [
|
||||
{
|
||||
label: "Contact Info",
|
||||
done: !!(guest.email || guest.phone),
|
||||
path: "/dashboard/profile"
|
||||
},
|
||||
{
|
||||
label: "Profile Photo",
|
||||
done: !!guest.avatar || !!(guest as any).profilePhoto, // Check both fields
|
||||
path: "/dashboard/profile"
|
||||
},
|
||||
{
|
||||
label: "Who's Who",
|
||||
done: !!guest.whosWhoOptIn || (!!guest.relationship && guest.relationship !== 'Guest'),
|
||||
path: "/dashboard/whos-who"
|
||||
},
|
||||
{
|
||||
label: "Guestbook Photo",
|
||||
done: !!guest.guestbookPhotoUrl,
|
||||
path: "/dashboard/guestbook"
|
||||
}
|
||||
];
|
||||
|
||||
const completedCount = checks.filter(c => c.done).length;
|
||||
const total = checks.length;
|
||||
const percentage = Math.round((completedCount / total) * 100);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Welcome Banner */}
|
||||
<div className="bg-white p-6 rounded-xl border shadow-sm">
|
||||
<h1 className="font-serif text-3xl text-wedding-evergreen mb-2">
|
||||
Welcome back, {guest.name.split(' ')[0]}!
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
We're counting down the days until we celebrate with you.
|
||||
</p>
|
||||
|
||||
{/* Progress Section */}
|
||||
<div className="mt-6 p-4 bg-neutral-50 rounded-lg border border-neutral-100">
|
||||
<div className="flex justify-between items-end mb-2">
|
||||
<div>
|
||||
<h3 className="font-medium text-wedding-ink">Profile Completion</h3>
|
||||
<p className="text-xs text-muted-foreground">Help us personalize your experience</p>
|
||||
</div>
|
||||
<span className="font-mono font-bold text-wedding-sage text-lg">{percentage}%</span>
|
||||
</div>
|
||||
<Progress value={percentage} className="h-2 mb-4" />
|
||||
|
||||
{percentage < 100 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium flex items-center gap-2 text-amber-600">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
Incomplete Items:
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{checks.filter(c => !c.done).map((item) => (
|
||||
<Link key={item.label} href={item.path} className="flex items-center justify-between p-2 bg-white rounded border text-sm hover:border-wedding-sage transition-colors group">
|
||||
<span>{item.label}</span>
|
||||
<ArrowRight className="w-3 h-3 text-muted-foreground group-hover:text-wedding-sage" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* RSVP Status Card */}
|
||||
<Card className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-lg font-serif text-wedding-evergreen">My RSVP</CardTitle>
|
||||
<span className={cn(
|
||||
"px-2 py-1 rounded text-xs font-bold uppercase",
|
||||
guest.rsvpStatus === 'ACCEPTED' ? "bg-green-100 text-green-700" :
|
||||
guest.rsvpStatus === 'DECLINED' ? "bg-red-100 text-red-700" :
|
||||
"bg-yellow-100 text-yellow-700"
|
||||
)}>
|
||||
{guest.rsvpStatus || "PENDING"}
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{guest.rsvpStatus === 'ACCEPTED'
|
||||
? `You're attending with ${guest.plusOnesCount || 0} guest(s).`
|
||||
: "You haven't confirmed your attendance yet."}
|
||||
</p>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href={`/rsvp/${guest.inviteCode}/step-2-accept-decline`}>Update RSVP</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Profile Card */}
|
||||
<Card className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-lg font-serif text-wedding-evergreen">My Profile</CardTitle>
|
||||
<UserCircle className="w-5 h-5 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
manage your contact info, dietary needs, and profile photo.
|
||||
</p>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/dashboard/profile">Edit Profile</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Who's Who Card */}
|
||||
<Card className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-lg font-serif text-wedding-evergreen">Who's Who</CardTitle>
|
||||
<Users className="w-5 h-5 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Share how you know the couple and find friends.
|
||||
</p>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/dashboard/whos-who">
|
||||
{guest.whosWhoOptIn ? "Update Entry" : "Join Family Tree"}
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Guestbook Card */}
|
||||
<Card className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-lg font-serif text-wedding-evergreen">Guestbook</CardTitle>
|
||||
<MessageSquare className="w-5 h-5 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{guest.guestbookMessage ? "Your message is saved." : "Write a note for the couple."}
|
||||
</p>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/dashboard/guestbook">
|
||||
{guest.guestbookMessage ? "View/Edit" : "Sign Guestbook"}
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <GuestDashboard />;
|
||||
}
|
||||
|
||||
41
src/app/(dashboard)/dashboard/story/page.tsx
Normal file
41
src/app/(dashboard)/dashboard/story/page.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useGuest } from "@/context/guest-context";
|
||||
import { Timeline } from "@/components/features/story/timeline";
|
||||
import { ArrowLeft, BookOpen } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function DashboardStoryPage() {
|
||||
const router = useRouter();
|
||||
const { guest } = useGuest();
|
||||
|
||||
if (!guest) return null;
|
||||
|
||||
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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Timeline />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
src/app/(dashboard)/dashboard/ticket/page.tsx
Normal file
41
src/app/(dashboard)/dashboard/ticket/page.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useGuest } from "@/context/guest-context";
|
||||
import { WeddingPass } from "@/components/features/ticket/wedding-pass";
|
||||
import { ArrowLeft, Ticket } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function DashboardTicketPage() {
|
||||
const router = useRouter();
|
||||
const { guest } = useGuest();
|
||||
|
||||
if (!guest) return null;
|
||||
|
||||
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">My Wedding Pass</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-md mx-auto space-y-8">
|
||||
<div className="bg-wedding-sage/10 p-4 rounded-lg flex gap-4 items-start border border-wedding-sage/20">
|
||||
<Ticket className="w-6 h-6 text-wedding-sage mt-1" />
|
||||
<div>
|
||||
<h3 className="font-medium text-wedding-evergreen">Your VIP Entry Pass 🎫</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Download or screenshot this pass. You'll need to present it at the security gate for entry to The Garden Venue.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WeddingPass />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
src/app/(dashboard)/dashboard/tribute/page.tsx
Normal file
31
src/app/(dashboard)/dashboard/tribute/page.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { useGuest } from "@/context/guest-context";
|
||||
import { TributeWall } from "@/components/features/tribute/tribute-wall";
|
||||
import { ArrowLeft, Flame } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function DashboardTributePage() {
|
||||
const router = useRouter();
|
||||
const { guest } = useGuest();
|
||||
|
||||
if (!guest) return null;
|
||||
|
||||
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">Tribute Wall</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto space-y-12">
|
||||
<TributeWall />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -94,6 +94,8 @@ const itemVariants = {
|
||||
* Dashboard Sections Mapping - Story Order
|
||||
* ========================= */
|
||||
const DASHBOARD_SECTIONS = [
|
||||
// Act I - Seating (gated)
|
||||
{ id: "seating", component: LazyTableReveal, label: "Seating", lazy: true },
|
||||
// Act 2 - The Plan
|
||||
{ id: "itinerary", component: LazyItineraryTimeline, label: "Schedule", lazy: true },
|
||||
{ id: "venue", component: LazyVenueMap, label: "Location", lazy: true },
|
||||
@@ -166,7 +168,7 @@ type NavItem = { kind: "route"; href: string } | { kind: "anchor"; id: string };
|
||||
export default function GuestDashboard() {
|
||||
const { guest, logout, isLoading, refresh } = useGuest();
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
// Track refreshing state to show smooth loading indicators
|
||||
const [isRefreshingGuest, setIsRefreshingGuest] = React.useState(false);
|
||||
const refreshTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -182,7 +184,7 @@ export default function GuestDashboard() {
|
||||
if (!silent) {
|
||||
setIsRefreshingGuest(true);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
if (refresh) {
|
||||
await refresh();
|
||||
@@ -205,7 +207,7 @@ export default function GuestDashboard() {
|
||||
|
||||
// Realtime updates subscription with debouncing
|
||||
const refreshDebounceRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!guest?.id) return;
|
||||
|
||||
@@ -220,7 +222,7 @@ export default function GuestDashboard() {
|
||||
// Subscribe to RSVP updates for this guest
|
||||
const unsubscribeRSVP = realtimeManager.subscribe('rsvp', (event: any) => {
|
||||
if (!isMounted) return;
|
||||
|
||||
|
||||
// Check if this update is for the current guest
|
||||
const eventGuestId = event.data?.guestId || event.data?.id || event.data?.guest?.id;
|
||||
if (eventGuestId === currentGuestId) {
|
||||
@@ -228,7 +230,7 @@ export default function GuestDashboard() {
|
||||
if (refreshDebounceRef.current) {
|
||||
clearTimeout(refreshDebounceRef.current);
|
||||
}
|
||||
|
||||
|
||||
// Immediate refresh for RSVP updates (don't debounce too much)
|
||||
refreshDebounceRef.current = setTimeout(() => {
|
||||
if (isMounted) {
|
||||
@@ -241,7 +243,7 @@ export default function GuestDashboard() {
|
||||
// Subscribe to seating assignment updates for this guest
|
||||
const unsubscribeSeating = realtimeManager.subscribe('seating', (event: any) => {
|
||||
if (!isMounted) return;
|
||||
|
||||
|
||||
// Check if this seating assignment is for the current guest
|
||||
const eventGuestId = event.data?.guestId;
|
||||
if (eventGuestId === currentGuestId) {
|
||||
@@ -249,7 +251,7 @@ export default function GuestDashboard() {
|
||||
if (refreshDebounceRef.current) {
|
||||
clearTimeout(refreshDebounceRef.current);
|
||||
}
|
||||
|
||||
|
||||
// Refresh guest data to get updated table assignment
|
||||
refreshDebounceRef.current = setTimeout(() => {
|
||||
if (isMounted) {
|
||||
@@ -297,7 +299,7 @@ export default function GuestDashboard() {
|
||||
// Debug logging in useEffect only
|
||||
React.useEffect(() => {
|
||||
if (!DEBUG) return;
|
||||
|
||||
|
||||
const logData = (location: string, message: string, data: any) => {
|
||||
// Only log in development if analytics endpoint is configured
|
||||
if (process.env.NODE_ENV !== 'development' || !process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT) {
|
||||
@@ -315,7 +317,7 @@ export default function GuestDashboard() {
|
||||
runId: 'run1',
|
||||
hypothesisId: 'D'
|
||||
})
|
||||
}).catch(() => {});
|
||||
}).catch(() => { });
|
||||
};
|
||||
|
||||
logData('dashboard-view.tsx:mount', 'Dashboard component mount', {
|
||||
@@ -476,7 +478,7 @@ export default function GuestDashboard() {
|
||||
className="min-h-screen relative pb-32 overflow-x-hidden"
|
||||
>
|
||||
{/* Background blobs */}
|
||||
|
||||
|
||||
|
||||
<div className="relative z-10 container max-w-lg mx-auto pt-8 px-4">
|
||||
{/* Header */}
|
||||
@@ -512,12 +514,12 @@ export default function GuestDashboard() {
|
||||
{/* Profile Avatar */}
|
||||
<div className="h-8 w-8 sm:h-10 sm:w-10 flex items-center justify-center rounded-full hover:bg-white/70 transition-all overflow-hidden border border-wedding-sage/20">
|
||||
<Avatar className="h-full w-full">
|
||||
<AvatarImage
|
||||
src={displayGuest?.avatar || undefined}
|
||||
alt={displayGuest?.name ? `${displayGuest.name}'s avatar` : 'Guest avatar'}
|
||||
<AvatarImage
|
||||
src={displayGuest?.avatar || undefined}
|
||||
alt={displayGuest?.name ? `${displayGuest.name}'s avatar` : 'Guest avatar'}
|
||||
/>
|
||||
<AvatarFallback className="bg-wedding-sage/10 text-wedding-evergreen text-xs sm:text-sm font-medium">
|
||||
{displayGuest?.name
|
||||
{displayGuest?.name
|
||||
? displayGuest.name.charAt(0).toUpperCase() + ((displayGuest as any).surname?.charAt(0) || '').toUpperCase()
|
||||
: 'G'
|
||||
}
|
||||
@@ -560,7 +562,7 @@ export default function GuestDashboard() {
|
||||
try {
|
||||
const hasResponded = displayGuest?.hasResponded;
|
||||
const isAttending = displayGuest?.isAttending === true;
|
||||
|
||||
|
||||
if (hasResponded && isAttending) {
|
||||
// Only show ticket if attending
|
||||
router.push("/dashboard/ticket");
|
||||
@@ -777,7 +779,7 @@ export default function GuestDashboard() {
|
||||
We can't wait to share this day with you...
|
||||
</p>
|
||||
<p className="text-base text-wedding-ink/70 leading-relaxed max-w-md mx-auto">
|
||||
Your presence will make our celebration complete.
|
||||
Your presence will make our celebration complete.
|
||||
Thank you for being part of our story.
|
||||
</p>
|
||||
<div className="pt-6">
|
||||
@@ -820,7 +822,7 @@ const RSVPStatusCard = ({
|
||||
const hasResponded = !!guest?.hasResponded;
|
||||
const isAttending = guest?.isAttending === true;
|
||||
const isNotAttending = guest?.isAttending === false;
|
||||
|
||||
|
||||
// Status determination with explicit checks
|
||||
let status: 'pending' | 'attending' | 'declined';
|
||||
if (!hasResponded || guest?.isAttending === null || guest?.isAttending === undefined) {
|
||||
@@ -832,10 +834,10 @@ const RSVPStatusCard = ({
|
||||
} else {
|
||||
status = 'pending'; // Fallback
|
||||
}
|
||||
|
||||
|
||||
const prevStatus = React.useRef(status);
|
||||
const [isAnimating, setIsAnimating] = React.useState(false);
|
||||
|
||||
|
||||
// Track state changes for smooth transitions
|
||||
React.useEffect(() => {
|
||||
if (prevStatus.current !== status) {
|
||||
@@ -962,7 +964,7 @@ const RSVPStatusCard = ({
|
||||
RSVP Status
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<motion.div
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={{ scale: 1, rotate: 0 }}
|
||||
@@ -974,13 +976,13 @@ const RSVPStatusCard = ({
|
||||
status === 'declined' && "bg-wedding-ink/5"
|
||||
)}
|
||||
>
|
||||
<StatusIcon
|
||||
<StatusIcon
|
||||
className={cn(
|
||||
"h-6 w-6",
|
||||
status === 'attending' && "text-wedding-sage",
|
||||
status === 'pending' && "text-amber-500",
|
||||
status === 'declined' && "text-wedding-ink/60"
|
||||
)}
|
||||
)}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -1108,7 +1110,7 @@ const RSVPStatusCard = ({
|
||||
const DashboardFloatingNav = () => {
|
||||
const router = useRouter();
|
||||
const [isConciergeOpen, setIsConciergeOpen] = React.useState(false);
|
||||
|
||||
|
||||
const navItems: FloatingNavItem[] = React.useMemo(
|
||||
() => [
|
||||
{ id: "home", label: "Home", icon: Home },
|
||||
@@ -1148,7 +1150,7 @@ const DashboardFloatingNav = () => {
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{isConciergeOpen && (
|
||||
<LazyWeddingConcierge
|
||||
<LazyWeddingConcierge
|
||||
isOpen={isConciergeOpen}
|
||||
onClose={() => setIsConciergeOpen(false)}
|
||||
/>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -61,11 +61,11 @@ function getPrismaInstance(): any {
|
||||
if (typeof process === 'undefined') {
|
||||
throw new Error('Prisma client can only be used in server-side code with Node.js environment.');
|
||||
}
|
||||
|
||||
|
||||
if (!process.env) {
|
||||
throw new Error('Prisma client can only be used in server-side code with Node.js environment.');
|
||||
}
|
||||
|
||||
|
||||
// CRITICAL: Check for DATABASE_URL - this is where the error occurs
|
||||
// We must check this AFTER confirming we're in a proper Node.js runtime
|
||||
// During bundling, process.env might exist but not have the actual values
|
||||
@@ -74,7 +74,7 @@ function getPrismaInstance(): any {
|
||||
// During bundling, this should never be called, but if it is, throw a clear error
|
||||
throw new Error('Prisma client cannot be initialized during bundling. This should never be called at build time.');
|
||||
}
|
||||
|
||||
|
||||
// Additional check: if DATABASE_URL is not set, check if we're in a bundling context
|
||||
const databaseUrl = process.env.RUNTIME_DATABASE_URL || process.env.DATABASE_URL;
|
||||
if (!databaseUrl) {
|
||||
@@ -90,14 +90,14 @@ function getPrismaInstance(): any {
|
||||
// Lazy require PrismaClient - only happens at runtime, not during bundling
|
||||
// Import at the top level but guard with runtime checks
|
||||
let PrismaClient: any;
|
||||
|
||||
|
||||
try {
|
||||
// Use a static import path but only execute at runtime
|
||||
// This avoids Turbopack's dynamic require issues
|
||||
const prismaClientModule = '@prisma/client';
|
||||
const prismaModule = require(prismaClientModule);
|
||||
PrismaClient = prismaModule.PrismaClient;
|
||||
|
||||
|
||||
if (!PrismaClient) {
|
||||
throw new Error('PrismaClient not found in @prisma/client module');
|
||||
}
|
||||
@@ -106,11 +106,12 @@ function getPrismaInstance(): any {
|
||||
throw new Error(`Prisma client could not be loaded: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
|
||||
// Try to use PrismaPg adapter if available
|
||||
let prismaAdapter: any = undefined;
|
||||
// Start Adapter (Required for engineType="client")
|
||||
let prismaAdapter: any;
|
||||
try {
|
||||
const { PrismaPg } = require('@prisma/adapter-pg');
|
||||
const { Pool } = require('pg');
|
||||
// Configure Pool
|
||||
const pool = new Pool({
|
||||
connectionString: databaseUrl,
|
||||
max: Number(process.env.PG_POOL_MAX || 10),
|
||||
@@ -119,17 +120,16 @@ function getPrismaInstance(): any {
|
||||
});
|
||||
prismaAdapter = new PrismaPg(pool);
|
||||
} catch (error) {
|
||||
// Adapter not available - will use default connection
|
||||
console.warn('PrismaPg adapter not available, using default connection:', error);
|
||||
console.error('Failed to initialize Prisma adapter:', error);
|
||||
throw new Error(`Prisma adapter initialization failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
// Create PrismaClient with adapter if available
|
||||
// Create PrismaClient with adapter (Required)
|
||||
const instance = new PrismaClient({
|
||||
log: isDevelopment ? ['query', 'error', 'warn'] : ['error'],
|
||||
// Pass the adapter if it was successfully created
|
||||
...(prismaAdapter && { adapter: prismaAdapter }),
|
||||
adapter: prismaAdapter,
|
||||
});
|
||||
|
||||
if (isDevelopment) {
|
||||
@@ -152,58 +152,58 @@ function createModelProxy(modelName: string): any {
|
||||
if (typeof window !== 'undefined') {
|
||||
throw new Error('Prisma client cannot be used in client components. Use API routes instead.');
|
||||
}
|
||||
|
||||
|
||||
if (typeof process === 'undefined' || !process.env) {
|
||||
throw new Error('Prisma client can only be used in server-side code with Node.js environment.');
|
||||
}
|
||||
|
||||
|
||||
// Initialize Prisma and get the model
|
||||
const instance = getPrismaInstance();
|
||||
|
||||
|
||||
// Prisma client uses camelCase for model names (e.g., "Tribute" -> "tribute")
|
||||
// The modelName parameter is already in camelCase from the proxy getter
|
||||
// Try the provided name first (most common case)
|
||||
let model = (instance as any)[modelName];
|
||||
|
||||
|
||||
// If model not found, try variations for case sensitivity
|
||||
if (!model) {
|
||||
// Try lowercase if provided name has uppercase
|
||||
if (modelName !== modelName.toLowerCase()) {
|
||||
model = (instance as any)[modelName.toLowerCase()];
|
||||
}
|
||||
|
||||
|
||||
// Try with first letter capitalized (schema convention)
|
||||
if (!model) {
|
||||
const capitalized = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
||||
model = (instance as any)[capitalized];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!model) {
|
||||
// Provide helpful error message with available models
|
||||
// Filter out internal Prisma properties and methods
|
||||
const availableModels = Object.keys(instance).filter(key =>
|
||||
!key.startsWith('$') &&
|
||||
!key.startsWith('_') &&
|
||||
const availableModels = Object.keys(instance).filter(key =>
|
||||
!key.startsWith('$') &&
|
||||
!key.startsWith('_') &&
|
||||
typeof (instance as any)[key] === 'object' &&
|
||||
(instance as any)[key] !== null
|
||||
);
|
||||
|
||||
|
||||
// Check if this might be a Next.js cache issue
|
||||
const errorMsg = `Prisma model '${modelName}' not found. ` +
|
||||
`Available models: ${availableModels.length > 0 ? availableModels.join(', ') : 'none'}. ` +
|
||||
`If you recently added this model, try: 1) Restart the Next.js dev server, 2) Run 'npx prisma generate', 3) Clear .next cache`;
|
||||
|
||||
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
|
||||
const method = model[methodName];
|
||||
|
||||
|
||||
if (typeof method === 'function') {
|
||||
// Call the Prisma method with the provided arguments
|
||||
return method.apply(model, args);
|
||||
}
|
||||
|
||||
|
||||
// Return the property value if it's not a function
|
||||
return method;
|
||||
};
|
||||
@@ -220,9 +220,9 @@ export const prisma = new Proxy({} as any, {
|
||||
// CRITICAL: This getter is called during module analysis/bundling
|
||||
// We must return a Proxy immediately without ANY server-side code execution
|
||||
// The Proxy will only initialize Prisma when actually invoked at runtime
|
||||
|
||||
|
||||
const propStr = String(prop);
|
||||
|
||||
|
||||
// For Prisma models (anything that's not a special Prisma property starting with $)
|
||||
// Return a nested Proxy that will handle method calls at runtime
|
||||
// This works for all models dynamically without hardcoding names
|
||||
@@ -231,7 +231,7 @@ export const prisma = new Proxy({} as any, {
|
||||
// Prisma client uses camelCase for model names (e.g., "Tribute" -> "tribute")
|
||||
return createModelProxy(propStr);
|
||||
}
|
||||
|
||||
|
||||
// For special Prisma properties (like $connect, $disconnect, etc.), return a function stub
|
||||
return function prismaStub(...args: any[]) {
|
||||
// This function is only called at actual runtime, not during bundling
|
||||
@@ -239,20 +239,20 @@ export const prisma = new Proxy({} as any, {
|
||||
if (typeof window !== 'undefined') {
|
||||
throw new Error('Prisma client cannot be used in client components. Use API routes instead.');
|
||||
}
|
||||
|
||||
|
||||
// Check if we're in a proper Node.js runtime
|
||||
if (typeof process === 'undefined' || !process.env) {
|
||||
throw new Error('Prisma client can only be used in server-side code with Node.js environment.');
|
||||
}
|
||||
|
||||
|
||||
// Only now, at actual runtime, do we initialize Prisma
|
||||
const instance = getPrismaInstance();
|
||||
const method = (instance as any)[prop];
|
||||
|
||||
|
||||
if (typeof method === 'function') {
|
||||
return method.apply(instance, args);
|
||||
}
|
||||
|
||||
|
||||
return method;
|
||||
};
|
||||
},
|
||||
|
||||
@@ -10,7 +10,15 @@ export interface Guest {
|
||||
phone?: string;
|
||||
dietaryRestrictions?: string;
|
||||
rsvpTimestamp?: string;
|
||||
avatar?: string; // Profile picture uploaded during RSVP (when socialOptIn is true)
|
||||
avatar?: string;
|
||||
profilePhoto?: string; // Fallback for avatar
|
||||
rsvpStatus?: 'ACCEPTED' | 'DECLINED' | 'PENDING';
|
||||
whosWhoOptIn?: boolean;
|
||||
relationship?: string;
|
||||
relationshipDetails?: string;
|
||||
guestbookMessage?: string;
|
||||
guestbookPhotoUrl?: string;
|
||||
plusOnesCount?: number;
|
||||
// New Optional Field
|
||||
table?: {
|
||||
id: number;
|
||||
|
||||
Reference in New Issue
Block a user