feat(client): add loading skeleton and global error handling components
Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
@@ -3,38 +3,40 @@ 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 { EventGridSkeleton } from './event-grid.skeleton';
|
||||||
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';
|
||||||
|
|
||||||
export function EventGridContainer() {
|
export function EventGridContainer() {
|
||||||
const { data, isLoading } = useGetEvents();
|
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 (
|
return (
|
||||||
<EventGridView
|
isLoading
|
||||||
events={allEvents}
|
? <EventGridSkeleton />
|
||||||
footer={eventInfo => (eventInfo.isJoined
|
: (
|
||||||
? <Button className="w-full" disabled>已加入</Button>
|
<EventGridView
|
||||||
: (
|
events={data.pages.flatMap(page => page.data ?? []).map(it => ({
|
||||||
<KycDialogContainer eventIdToJoin={eventInfo.eventId}>
|
type: it.type! as EventInfo['type'],
|
||||||
<DialogTrigger asChild>
|
eventId: it.event_id!,
|
||||||
<Button className="w-full">加入活动</Button>
|
isJoined: it.is_joined!,
|
||||||
</DialogTrigger>
|
requireKyc: it.enable_kyc!,
|
||||||
</KycDialogContainer>
|
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>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
16
client/cms/src/components/events/event-grid.error.tsx
Normal file
16
client/cms/src/components/events/event-grid.error.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { EventCardSkeleton } from './event-card.skeleton';
|
|||||||
export function EventGridSkeleton() {
|
export function EventGridSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
<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
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<EventCardSkeleton key={i} />
|
<EventCardSkeleton key={i} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function EventGridView({ events, footer }: { events: EventInfo[]; footer:
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{events.length > 0 && (
|
{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 => (
|
{events.map(event => (
|
||||||
<EventCardView key={event.eventId} eventInfo={event} actionFooter={footer(event)} />
|
<EventCardView key={event.eventId} eventInfo={event} actionFooter={footer(event)} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
18
client/cms/src/components/global.error.tsx
Normal file
18
client/cms/src/components/global.error.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,3 +4,13 @@ export interface RawError {
|
|||||||
error_id: string;
|
error_id: string;
|
||||||
data: null;
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { createRootRoute, Outlet } from '@tanstack/react-router';
|
import { createRootRoute, Outlet } from '@tanstack/react-router';
|
||||||
|
import { GlobalError } from '@/components/global.error';
|
||||||
import { ThemeProvider } from '@/components/theme-provider';
|
import { ThemeProvider } from '@/components/theme-provider';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import '@/index.css';
|
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} /> });
|
||||||
|
|||||||
Reference in New Issue
Block a user