From c43c37a127d36177fe1a346168e75b18ac0ae113 Mon Sep 17 00:00:00 2001 From: Noa Virellia Date: Fri, 6 Feb 2026 21:36:58 +0800 Subject: [PATCH] feat(client): add loading skeleton and global error handling components Signed-off-by: Noa Virellia --- .../events/event-grid.container.tsx | 54 ++++++++++--------- .../components/events/event-grid.error.tsx | 16 ++++++ .../components/events/event-grid.skeleton.tsx | 2 +- .../src/components/events/event-grid.view.tsx | 2 +- client/cms/src/components/global.error.tsx | 18 +++++++ client/cms/src/lib/types.ts | 10 ++++ client/cms/src/routes/__root.tsx | 3 +- 7 files changed, 76 insertions(+), 29 deletions(-) create mode 100644 client/cms/src/components/events/event-grid.error.tsx create mode 100644 client/cms/src/components/global.error.tsx diff --git a/client/cms/src/components/events/event-grid.container.tsx b/client/cms/src/components/events/event-grid.container.tsx index 1392e4c..888a1c5 100644 --- a/client/cms/src/components/events/event-grid.container.tsx +++ b/client/cms/src/components/events/event-grid.container.tsx @@ -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 ( - (eventInfo.isJoined - ? - : ( - - - - - - ) - )} - /> + isLoading + ? + : ( + 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 + ? + : ( + + + + + + ) + )} + /> + ) ); } diff --git a/client/cms/src/components/events/event-grid.error.tsx b/client/cms/src/components/events/event-grid.error.tsx new file mode 100644 index 0000000..ad2bace --- /dev/null +++ b/client/cms/src/components/events/event-grid.error.tsx @@ -0,0 +1,16 @@ +import { FileExclamationPoint } from 'lucide-react'; +import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty'; + +export function EventGridError() { + return ( + + + + + + 活动列表加载失败 + 前面的区域 以后再来探索吧 + + + ); +} diff --git a/client/cms/src/components/events/event-grid.skeleton.tsx b/client/cms/src/components/events/event-grid.skeleton.tsx index 7c0046a..650bd67 100644 --- a/client/cms/src/components/events/event-grid.skeleton.tsx +++ b/client/cms/src/components/events/event-grid.skeleton.tsx @@ -3,7 +3,7 @@ import { EventCardSkeleton } from './event-card.skeleton'; export function EventGridSkeleton() { return (
- {Array.from({ length: 8 }).map((_, i) => ( + {Array.from({ length: 4 }).map((_, i) => ( // eslint-disable-next-line react/no-array-index-key ))} diff --git a/client/cms/src/components/events/event-grid.view.tsx b/client/cms/src/components/events/event-grid.view.tsx index 8f1e517..fbe6b09 100644 --- a/client/cms/src/components/events/event-grid.view.tsx +++ b/client/cms/src/components/events/event-grid.view.tsx @@ -6,7 +6,7 @@ export function EventGridView({ events, footer }: { events: EventInfo[]; footer: return ( <> {events.length > 0 && ( -
+
{events.map(event => ( ))} diff --git a/client/cms/src/components/global.error.tsx b/client/cms/src/components/global.error.tsx new file mode 100644 index 0000000..9069741 --- /dev/null +++ b/client/cms/src/components/global.error.tsx @@ -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 ( + + + + + + 出错了 + {isRawError(error) ? error.error_id : error.message} + + + ); +} diff --git a/client/cms/src/lib/types.ts b/client/cms/src/lib/types.ts index fa81fc9..22ed52c 100644 --- a/client/cms/src/lib/types.ts +++ b/client/cms/src/lib/types.ts @@ -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 + ); +} diff --git a/client/cms/src/routes/__root.tsx b/client/cms/src/routes/__root.tsx index 1f0df9e..d6952eb 100644 --- a/client/cms/src/routes/__root.tsx +++ b/client/cms/src/routes/__root.tsx @@ -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 }) => });