Refactor of dashboard page with breadcrumbs and cleaner component

This commit is contained in:
2025-03-16 17:10:05 +01:00
parent b9bff35122
commit df1256996b
6 changed files with 379 additions and 86 deletions

View File

@@ -0,0 +1,25 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
export default function EventsRedirectPage() {
const router = useRouter();
useEffect(() => {
// Redirect to dashboard home page
router.push("/dashboard");
}, [router]);
// Show a loading state while redirecting
return (
<div className="flex items-center justify-center min-h-[60vh]">
<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 mb-4"></div>
<p className="text-gray-500 dark:text-gray-400">
Redirecting to dashboard...
</p>
</div>
</div>
);
}

View File

@@ -1,9 +1,10 @@
"use client";
import { useAuth } from "@/context/auth-context";
import { useRouter } from "next/navigation";
import { useRouter, usePathname } from "next/navigation";
import { useEffect } from "react";
import Navbar from "@/components/layout/navbar";
import Breadcrumbs from "@/components/layout/breadcrumb";
export default function MainLayout({
children,
@@ -12,6 +13,7 @@ export default function MainLayout({
}) {
const { isAuthenticated, isLoading } = useAuth();
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
@@ -38,10 +40,20 @@ export default function MainLayout({
);
}
// Don't show breadcrumbs on the main dashboard page
const showBreadcrumbs = pathname !== "/dashboard";
return (
<>
<Navbar />
<div className="container mx-auto px-4 py-12">{children}</div>
<div className="container mx-auto px-4 py-12">
{showBreadcrumbs && (
<div className="mb-6">
<Breadcrumbs />
</div>
)}
{children}
</div>
</>
);
}

View File

@@ -1,27 +1,12 @@
"use client";
import Navbar from "@/components/layout/navbar";
import { useEvents } from "@/context/event-context";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Loader2Icon, CalendarIcon, MapPinIcon } from "lucide-react";
import { EventResponse } from "@/client";
import { useEvents } from "@/context/event-context";
import EventsGrid from "@/components/events/events-grid";
export default function DashboardPage() {
const {
userEvents,
isLoadingUserEvents,
userEvents: eventsData,
} = useEvents();
const { userEvents, isLoadingUserEvents } = useEvents();
return (
<>
@@ -45,72 +30,13 @@ export default function DashboardPage() {
</div>
</div>
{isLoadingUserEvents && (
<div className="flex items-center gap-2">
<Loader2Icon className="h-6 w-6 animate-spin text-blue-500" />
<span>Loading your events...</span>
</div>
)}
{!isLoadingUserEvents &&
userEvents?.items &&
userEvents.items.length > 0 && (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{userEvents.items.map((event: EventResponse) => (
<Card
key={event.id}
className="hover:shadow-lg transition-shadow"
>
<CardHeader>
<CardTitle>{event.title}</CardTitle>
{event.is_public ? (
<Badge variant="default">Public</Badge>
) : (
<Badge variant="secondary">Private</Badge>
)}
</CardHeader>
<CardContent>
<CardDescription className="line-clamp-2">
{event.description || "No description provided."}
</CardDescription>
{event.location_address && (
<div className="flex items-center gap-2 mt-2 text-sm text-gray-500 dark:text-gray-400">
<MapPinIcon className="h-4 w-4" />
{event.location_address}
</div>
)}
{event.event_start_time && (
<div className="flex items-center gap-2 mt-1 text-sm text-gray-500 dark:text-gray-400">
<CalendarIcon className="h-4 w-4" />
{new Date(event.event_start_time).toLocaleString()}
</div>
)}
</CardContent>
<CardFooter>
<Button size="sm" asChild>
<Link href={`/dashboard/events/${event.slug}`}>
View Event
</Link>
</Button>
</CardFooter>
</Card>
))}
</div>
)}
{!isLoadingUserEvents &&
(!userEvents?.items || userEvents.items.length === 0) && (
<div className="bg-gray-100 dark:bg-slate-700 rounded p-8 text-center">
<p className="text-gray-500 dark:text-gray-400">
You haven't created any events yet.
</p>
<Button asChild className="mt-4">
<Link href="/dashboard/events/new">
Create Your First Event
</Link>
</Button>
</div>
)}
<EventsGrid
events={userEvents}
isLoading={isLoadingUserEvents}
emptyMessage="You haven't created any events yet."
emptyActionLink="/dashboard/events/new"
emptyActionText="Create Your First Event"
/>
</div>
</>
);

View File

@@ -0,0 +1,97 @@
import React from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Loader2Icon, CalendarIcon, MapPinIcon } from "lucide-react";
import { EventResponse } from "@/client";
interface EventsGridProps {
events?: {
items: EventResponse[];
};
isLoading?: boolean;
emptyMessage?: string;
emptyActionLink?: string;
emptyActionText?: string;
}
const EventsGrid: React.FC<EventsGridProps> = ({
events,
isLoading = false,
emptyMessage = "No events found.",
emptyActionLink,
emptyActionText,
}) => {
return (
<>
{isLoading && (
<div className="flex items-center gap-2">
<Loader2Icon className="h-6 w-6 animate-spin text-blue-500" />
<span>Loading events...</span>
</div>
)}
{!isLoading && events?.items && events.items.length > 0 && (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{events.items.map((event: EventResponse) => (
<Card key={event.id} className="hover:shadow-lg transition-shadow">
<CardHeader>
<CardTitle>{event.title}</CardTitle>
{event.is_public ? (
<Badge variant="default">Public</Badge>
) : (
<Badge variant="secondary">Private</Badge>
)}
</CardHeader>
<CardContent>
<CardDescription className="line-clamp-2">
{event.description || "No description provided."}
</CardDescription>
{event.location_address && (
<div className="flex items-center gap-2 mt-2 text-sm text-gray-500 dark:text-gray-400">
<MapPinIcon className="h-4 w-4" />
{event.location_address}
</div>
)}
{event.event_start_time && (
<div className="flex items-center gap-2 mt-1 text-sm text-gray-500 dark:text-gray-400">
<CalendarIcon className="h-4 w-4" />
{new Date(event.event_start_time).toLocaleString()}
</div>
)}
</CardContent>
<CardFooter>
<Button size="sm" asChild>
<Link href={`/dashboard/events/${event.slug}`}>
View Event
</Link>
</Button>
</CardFooter>
</Card>
))}
</div>
)}
{!isLoading && (!events?.items || events.items.length === 0) && (
<div className="bg-gray-100 dark:bg-slate-700 rounded p-8 text-center">
<p className="text-gray-500 dark:text-gray-400">{emptyMessage}</p>
{emptyActionLink && emptyActionText && (
<Button asChild className="mt-4">
<Link href={emptyActionLink}>{emptyActionText}</Link>
</Button>
)}
</div>
)}
</>
);
};
export default EventsGrid;

View File

@@ -0,0 +1,124 @@
"use client";
import React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ChevronRight, HomeIcon } from "lucide-react";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
interface BreadcrumbItemData {
label: string;
href: string;
isActive: boolean;
}
interface BreadcrumbsProps {
homeHref?: string;
homeLabel?: React.ReactNode;
className?: string;
items?: BreadcrumbItemData[]; // Optional manual items
}
const Breadcrumbs = ({
homeHref = "/dashboard",
homeLabel = <HomeIcon className="h-4 w-4" />,
className = "",
items,
}: BreadcrumbsProps) => {
const pathname = usePathname();
// Only generate items from pathname if no items provided
const breadcrumbItems = items || generateBreadcrumbItems(pathname);
return (
<Breadcrumb className={className}>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href={homeHref} aria-label="Dashboard">
{homeLabel}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
{breadcrumbItems.map((item, index) => (
<React.Fragment key={index}>
<BreadcrumbSeparator>
<ChevronRight className="h-4 w-4" />
</BreadcrumbSeparator>
<BreadcrumbItem>
{item.isActive ? (
<BreadcrumbPage>{item.label}</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link href={item.href}>{item.label}</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
);
};
// Helper function to generate breadcrumb items from pathname
function generateBreadcrumbItems(pathname: string): BreadcrumbItemData[] {
if (pathname === "/dashboard") return [];
// Remove leading /dashboard from pathname
const path = pathname.replace(/^\/dashboard\/?/, "");
if (!path) return [];
// Split path into segments
const segments = path.split("/").filter(Boolean);
// Map of slugs to human-readable labels
const segmentLabels: Record<string, string> = {
events: "Events",
"event-themes": "Event Themes",
new: "New",
edit: "Edit",
gifts: "Gifts",
};
const items: BreadcrumbItemData[] = [];
let currentPath = "/dashboard";
segments.forEach((segment, index) => {
currentPath += `/${segment}`;
// Check if segment is a dynamic route (starts with [])
// In actual URLs, dynamic segments don't have brackets
let label =
segmentLabels[segment] ||
(segment.startsWith("[") && segment.endsWith("]")
? segment.slice(1, -1).charAt(0).toUpperCase() + segment.slice(2, -1)
: segment.charAt(0).toUpperCase() + segment.slice(1));
// Special handling for IDs/slugs - use ID/Slug instead of the actual value
// This assumes IDs/slugs are at odd positions in the path (events/[id], themes/[id])
if (index % 2 === 1 && segments[index - 1] === "events") {
label = "Event Details";
} else if (index % 2 === 1 && segments[index - 1] === "event-themes") {
label = "Theme Details";
}
items.push({
label,
href: currentPath,
isActive: currentPath === pathname,
});
});
return items;
}
export default Breadcrumbs;

View File

@@ -0,0 +1,109 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}