Refactor of dashboard page with breadcrumbs and cleaner component
This commit is contained in:
25
frontend/src/app/(main)/dashboard/events/page.tsx
Normal file
25
frontend/src/app/(main)/dashboard/events/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAuth } from "@/context/auth-context";
|
import { useAuth } from "@/context/auth-context";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import Navbar from "@/components/layout/navbar";
|
import Navbar from "@/components/layout/navbar";
|
||||||
|
import Breadcrumbs from "@/components/layout/breadcrumb";
|
||||||
|
|
||||||
export default function MainLayout({
|
export default function MainLayout({
|
||||||
children,
|
children,
|
||||||
@@ -12,6 +13,7 @@ export default function MainLayout({
|
|||||||
}) {
|
}) {
|
||||||
const { isAuthenticated, isLoading } = useAuth();
|
const { isAuthenticated, isLoading } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && !isAuthenticated) {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Navbar from "@/components/layout/navbar";
|
|
||||||
import { useEvents } from "@/context/event-context";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { useEvents } from "@/context/event-context";
|
||||||
Card,
|
import EventsGrid from "@/components/events/events-grid";
|
||||||
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";
|
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const {
|
const { userEvents, isLoadingUserEvents } = useEvents();
|
||||||
userEvents,
|
|
||||||
isLoadingUserEvents,
|
|
||||||
userEvents: eventsData,
|
|
||||||
} = useEvents();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -45,72 +30,13 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoadingUserEvents && (
|
<EventsGrid
|
||||||
<div className="flex items-center gap-2">
|
events={userEvents}
|
||||||
<Loader2Icon className="h-6 w-6 animate-spin text-blue-500" />
|
isLoading={isLoadingUserEvents}
|
||||||
<span>Loading your events...</span>
|
emptyMessage="You haven't created any events yet."
|
||||||
</div>
|
emptyActionLink="/dashboard/events/new"
|
||||||
)}
|
emptyActionText="Create Your First Event"
|
||||||
|
/>
|
||||||
{!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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
97
frontend/src/components/events/events-grid.tsx
Normal file
97
frontend/src/components/events/events-grid.tsx
Normal 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;
|
||||||
124
frontend/src/components/layout/breadcrumb.tsx
Normal file
124
frontend/src/components/layout/breadcrumb.tsx
Normal 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;
|
||||||
109
frontend/src/components/ui/breadcrumb.tsx
Normal file
109
frontend/src/components/ui/breadcrumb.tsx
Normal 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,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user