Add invitation and dashboard pages with UI components
Introduced a dynamic invitation page to display event details based on a slug, including RSVP functionality (placeholder). Added a dashboard page for authenticated users, providing a welcome message and placeholder for event management. Also implemented reusable Card UI components for consistent styling.
This commit is contained in:
58
frontend/src/app/(main)/dashboard/events/[id]/page.tsx
Normal file
58
frontend/src/app/(main)/dashboard/events/[id]/page.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// src/app/(main)/dashboard/page.tsx
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useAuth } from "@/context/auth-context";
|
||||||
|
import Navbar from "@/components/layout/navbar";
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
const { user, isLoading } = useAuth();
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="h-8 w-8 mx-auto border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin"></div>
|
||||||
|
<p className="mt-2">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar />
|
||||||
|
<div className="container mx-auto px-4 py-12">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold mb-8">Dashboard</h1>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">
|
||||||
|
Welcome, {user?.first_name || "User"}!
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
You are now logged in to EventSpace.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="text-lg font-medium mb-4">Your Events</h3>
|
||||||
|
<div className="bg-gray-100 dark:bg-slate-700 rounded p-8 text-center">
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
No events yet
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||||
|
onClick={() =>
|
||||||
|
alert("Create event functionality coming soon!")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Create Your First Event
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
184
frontend/src/app/(public)/invite/[slug]/page.tsx
Normal file
184
frontend/src/app/(public)/invite/[slug]/page.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
"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 Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEvents } from "@/context/event-context";
|
||||||
|
import { EventResponse } from "@/client/types.gen";
|
||||||
|
|
||||||
|
const InvitationPage = () => {
|
||||||
|
const { slug } = useParams();
|
||||||
|
const { getEventBySlug } = useEvents();
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [event, setEvent] = useState<EventResponse | null>(null);
|
||||||
|
const [notFound, setNotFound] = useState(false);
|
||||||
|
const [showRSVP, setShowRSVP] = useState(false);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notFound || !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.
|
||||||
|
</p>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/">Go Back Home</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
className="text-4xl sm:text-5xl md:text-6xl font-bold text-emerald-700 mb-2"
|
||||||
|
style={{ fontFamily: "var(--font-bubblegum)" }}
|
||||||
|
>
|
||||||
|
{event.title}
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl sm:text-2xl text-amber-600 font-medium">
|
||||||
|
{event.description}
|
||||||
|
</p>
|
||||||
|
</motion.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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InvitationPage;
|
||||||
68
frontend/src/components/ui/card.tsx
Normal file
68
frontend/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
Reference in New Issue
Block a user