feat(client): add loading skeleton and global error handling components
All checks were successful
Client CMS Check Build (NixCN CMS) TeamCity build finished
Backend Check Build (NixCN CMS) TeamCity build finished

Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
2026-02-06 21:36:58 +08:00
parent 5cf00407b4
commit c43c37a127
7 changed files with 76 additions and 29 deletions

View File

@@ -3,38 +3,40 @@ import PlaceholderImage from '@/assets/event-placeholder.png';
import { useGetEvents } from '@/hooks/data/useGetEvents';
import { Button } from '../ui/button';
import { DialogTrigger } from '../ui/dialog';
import { EventGridSkeleton } from './event-grid.skeleton';
import { EventGridView } from './event-grid.view';
import { KycDialogContainer } from './kyc/kyc.dialog.container';
export function EventGridContainer() {
const { data, isLoading } = useGetEvents();
const allEvents: EventInfo[] = isLoading
? []
: data.pages.flatMap(page => page.data ?? []).map(it => ({
type: it.type! as EventInfo['type'],
eventId: it.event_id!,
isJoined: it.is_joined!,
requireKyc: it.enable_kyc!,
coverImage: it.thumbnail! || PlaceholderImage,
eventName: it.name!,
description: it.description!,
startTime: new Date(it.start_time!),
endTime: new Date(it.end_time!),
} satisfies EventInfo));
return (
<EventGridView
events={allEvents}
footer={eventInfo => (eventInfo.isJoined
? <Button className="w-full" disabled></Button>
: (
<KycDialogContainer eventIdToJoin={eventInfo.eventId}>
<DialogTrigger asChild>
<Button className="w-full"></Button>
</DialogTrigger>
</KycDialogContainer>
)
)}
/>
isLoading
? <EventGridSkeleton />
: (
<EventGridView
events={data.pages.flatMap(page => page.data ?? []).map(it => ({
type: it.type! as EventInfo['type'],
eventId: it.event_id!,
isJoined: it.is_joined!,
requireKyc: it.enable_kyc!,
coverImage: it.thumbnail! || PlaceholderImage,
eventName: it.name!,
description: it.description!,
startTime: new Date(it.start_time!),
endTime: new Date(it.end_time!),
} satisfies EventInfo))}
footer={eventInfo => (eventInfo.isJoined
? <Button className="w-full" disabled></Button>
: (
<KycDialogContainer eventIdToJoin={eventInfo.eventId}>
<DialogTrigger asChild>
<Button className="w-full"></Button>
</DialogTrigger>
</KycDialogContainer>
)
)}
/>
)
);
}

View File

@@ -0,0 +1,16 @@
import { FileExclamationPoint } from 'lucide-react';
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty';
export function EventGridError() {
return (
<Empty className="h-full">
<EmptyHeader>
<EmptyMedia variant="icon">
<FileExclamationPoint />
</EmptyMedia>
<EmptyTitle></EmptyTitle>
<EmptyDescription> </EmptyDescription>
</EmptyHeader>
</Empty>
);
}

View File

@@ -3,7 +3,7 @@ import { EventCardSkeleton } from './event-card.skeleton';
export function EventGridSkeleton() {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{Array.from({ length: 8 }).map((_, i) => (
{Array.from({ length: 4 }).map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
<EventCardSkeleton key={i} />
))}

View File

@@ -6,7 +6,7 @@ export function EventGridView({ events, footer }: { events: EventInfo[]; footer:
return (
<>
{events.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 h-full">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
{events.map(event => (
<EventCardView key={event.eventId} eventInfo={event} actionFooter={footer(event)} />
))}

View File

@@ -0,0 +1,18 @@
import type { RawError } from '@/lib/types';
import { TriangleAlert } from 'lucide-react';
import { isRawError } from '@/lib/types';
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from './ui/empty';
export function GlobalError({ error }: { error: Error | RawError }) {
return (
<Empty className="h-screen w-full">
<EmptyHeader>
<EmptyMedia variant="icon">
<TriangleAlert />
</EmptyMedia>
<EmptyTitle></EmptyTitle>
<EmptyDescription>{isRawError(error) ? error.error_id : error.message}</EmptyDescription>
</EmptyHeader>
</Empty>
);
}

View File

@@ -4,3 +4,13 @@ export interface RawError {
error_id: string;
data: null;
}
export function isRawError(obj: any): obj is RawError {
return (
typeof obj === 'object'
&& obj !== null
&& 'code' in obj
&& 'status' in obj
&& 'error_id' in obj
);
}

View File

@@ -1,5 +1,6 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createRootRoute, Outlet } from '@tanstack/react-router';
import { GlobalError } from '@/components/global.error';
import { ThemeProvider } from '@/components/theme-provider';
import { Toaster } from '@/components/ui/sonner';
import '@/index.css';
@@ -34,4 +35,4 @@ function RootLayout() {
);
}
export const Route = createRootRoute({ component: RootLayout });
export const Route = createRootRoute({ component: RootLayout, errorComponent: ({ error }) => <GlobalError error={error} /> });