feat: add empty state to events grid when no events exist
All checks were successful
Backend Check Build (NixCN CMS) TeamCity build finished
Client CMS Check Build (NixCN CMS) TeamCity build finished

Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
2026-02-05 19:53:46 +08:00
parent 7afc6ec25e
commit 45159484d9
3 changed files with 147 additions and 24 deletions

View File

@@ -1,8 +1,10 @@
import type { EventInfo } from './types'; import type { EventInfo } from './types';
import { FileQuestionMark } from 'lucide-react';
import PlaceholderImage from '@/assets/event-placeholder.png'; import PlaceholderImage from '@/assets/event-placeholder.png';
import { useGetEvents } from '@/hooks/data/useGetEvents'; import { useGetEvents } from '@/hooks/data/useGetEvents';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { DialogTrigger } from '../ui/dialog'; import { DialogTrigger } from '../ui/dialog';
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty';
import { EventGridView } from './event-grid.view'; import { EventGridView } from './event-grid.view';
import { KycDialogContainer } from './kyc/kyc.dialog.container'; import { KycDialogContainer } from './kyc/kyc.dialog.container';
@@ -10,31 +12,48 @@ export function EventGridContainer() {
const { data, isLoading } = useGetEvents(); const { data, isLoading } = useGetEvents();
const allEvents: EventInfo[] = isLoading const allEvents: EventInfo[] = isLoading
? [] ? []
: data.pages.flatMap(page => page.data!).map(it => ({ : data.pages.flatMap(page => page.data ?? []).map(it => ({
type: it.type! as EventInfo['type'], type: it.type! as EventInfo['type'],
eventId: it.event_id!, eventId: it.event_id!,
isJoined: it.is_joined!, isJoined: it.is_joined!,
requireKyc: it.enable_kyc!, requireKyc: it.enable_kyc!,
coverImage: it.thumbnail! || PlaceholderImage, coverImage: it.thumbnail! || PlaceholderImage,
eventName: it.name!, eventName: it.name!,
description: it.description!, description: it.description!,
startTime: new Date(it.start_time!), startTime: new Date(it.start_time!),
endTime: new Date(it.end_time!), endTime: new Date(it.end_time!),
} satisfies EventInfo)); } satisfies EventInfo));
return ( return (
<EventGridView <>
events={allEvents} {allEvents.length > 0 && (
assembleFooter={eventInfo => (eventInfo.isJoined <EventGridView
? <Button className="w-full" disabled></Button> events={allEvents}
: ( assembleFooter={eventInfo => (eventInfo.isJoined
<KycDialogContainer eventIdToJoin={eventInfo.eventId}> ? <Button className="w-full" disabled></Button>
<DialogTrigger asChild> : (
<Button className="w-full"></Button> <KycDialogContainer eventIdToJoin={eventInfo.eventId}>
</DialogTrigger> <DialogTrigger asChild>
</KycDialogContainer> <Button className="w-full"></Button>
) </DialogTrigger>
</KycDialogContainer>
)
)}
/>
)} )}
/> {
allEvents.length === 0 && (
<Empty className="h-full">
<EmptyHeader>
<EmptyMedia variant="icon">
<FileQuestionMark />
</EmptyMedia>
<EmptyTitle></EmptyTitle>
<EmptyDescription> </EmptyDescription>
</EmptyHeader>
</Empty>
)
}
</>
); );
} }

View File

@@ -0,0 +1,104 @@
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
className
)}
{...props}
/>
)
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-header"
className={cn(
"flex max-w-sm flex-col items-center gap-2 text-center",
className
)}
{...props}
/>
)
}
const emptyMediaVariants = cva(
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
},
},
defaultVariants: {
variant: "default",
},
}
)
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
)
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight", className)}
{...props}
/>
)
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
data-slot="empty-description"
className={cn(
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className
)}
{...props}
/>
)
}
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
}

View File

@@ -7,7 +7,7 @@ export const Route = createFileRoute('/_workbenchLayout/events')({
function RouteComponent() { function RouteComponent() {
return ( return (
<div className="py-4 px-6 md:gap-6 md:py-6"> <div className="py-4 px-6 md:gap-6 md:py-6 h-full">
<EventGridContainer /> <EventGridContainer />
</div> </div>
); );