diff --git a/client/cms/src/components/events/event-grid/event-grid.container.tsx b/client/cms/src/components/events/event-grid/event-grid.container.tsx index 02b545a..f2d258e 100644 --- a/client/cms/src/components/events/event-grid/event-grid.container.tsx +++ b/client/cms/src/components/events/event-grid/event-grid.container.tsx @@ -1,10 +1,9 @@ -import type { EventInfo } from '../types'; -import PlaceholderImage from '@/assets/event-placeholder.png'; -import { useGetEvents } from '@/hooks/data/useGetEvents'; +import { useEvents } from '@/hooks/data/useEvents'; import { Button } from '../../ui/button'; import { DialogTrigger } from '../../ui/dialog'; import { EventJoinDialogContainer } from '../event-join.dialog.container'; import { KycDialogContainer } from '../kyc/kyc.dialog.container'; +import { toEventInfo } from '../types'; import { EventGridSkeleton } from './event-grid.skeleton'; import { EventGridView } from './event-grid.view'; @@ -15,24 +14,14 @@ function JoinButton() { } export function EventGridContainer() { - const { data, isLoading } = useGetEvents(); + const { data, isLoading } = useEvents(); return ( 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))} + events={data.pages.flatMap(page => page.data ?? []).map(toEventInfo)} footer={eventInfo => (eventInfo.isJoined ? : ( diff --git a/client/cms/src/components/events/joined-events.containers.tsx b/client/cms/src/components/events/joined-events.containers.tsx new file mode 100644 index 0000000..4ebdc54 --- /dev/null +++ b/client/cms/src/components/events/joined-events.containers.tsx @@ -0,0 +1,33 @@ +import type { EventInfo } from './types'; +import { useJoinedEvents } from '@/hooks/data/useJoinedEvents'; +import { isInDateRange } from '@/lib/utils'; +import { Button } from '../ui/button'; +import { EventGridSkeleton } from './event-grid/event-grid.skeleton'; +import { EventGridView } from './event-grid/event-grid.view'; +import { toEventInfo } from './types'; + +export function JoinedEventGridFooter({ event }: { event: EventInfo }) { + return ( +
+ + +
+ ); +} + +export function JoinedEventsContainer() { + const { data, isLoading } = useJoinedEvents(); + + return ( + isLoading + ? + : ( + page.data ?? []).map(toEventInfo)} + footer={event => ( + + )} + /> + ) + ); +} diff --git a/client/cms/src/components/events/types.ts b/client/cms/src/components/events/types.ts index 6564ba3..930e3fc 100644 --- a/client/cms/src/components/events/types.ts +++ b/client/cms/src/components/events/types.ts @@ -1,3 +1,6 @@ +import type { DataEventIndexDoc } from '@/client'; +import PlaceholderImage from '@/assets/event-placeholder.png'; + export interface EventInfo { type: 'official' | 'party'; eventId: string; @@ -9,3 +12,17 @@ export interface EventInfo { startTime: Date; endTime: Date; } + +export function toEventInfo(raw: DataEventIndexDoc): EventInfo { + return { + type: raw.type! as EventInfo['type'], + eventId: raw.event_id!, + isJoined: raw.is_joined!, + requireKyc: raw.enable_kyc!, + coverImage: raw.thumbnail! || PlaceholderImage, + eventName: raw.name!, + description: raw.description!, + startTime: new Date(raw.start_time!), + endTime: new Date(raw.end_time!), + }; +} diff --git a/client/cms/src/hooks/data/useGetEvents.ts b/client/cms/src/hooks/data/useEvents.ts similarity index 94% rename from client/cms/src/hooks/data/useGetEvents.ts rename to client/cms/src/hooks/data/useEvents.ts index 8ba728a..f27c4cb 100644 --- a/client/cms/src/hooks/data/useGetEvents.ts +++ b/client/cms/src/hooks/data/useEvents.ts @@ -2,7 +2,7 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { isNil } from 'lodash-es'; import { getEventListInfiniteOptions } from '@/client/@tanstack/react-query.gen'; -export function useGetEvents() { +export function useEvents() { return useInfiniteQuery({ ...getEventListInfiniteOptions({ query: {}, diff --git a/client/cms/src/hooks/data/useJoinedEvents.ts b/client/cms/src/hooks/data/useJoinedEvents.ts new file mode 100644 index 0000000..bcbb2eb --- /dev/null +++ b/client/cms/src/hooks/data/useJoinedEvents.ts @@ -0,0 +1,19 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { isNil } from 'lodash-es'; +import { getEventJoinedInfiniteOptions } from '@/client/@tanstack/react-query.gen'; + +export function useJoinedEvents() { + return useInfiniteQuery({ + ...getEventJoinedInfiniteOptions({ + query: {}, + }), + initialPageParam: 0, + getNextPageParam: (lastPage, allPages) => { + const currentData = lastPage?.data; + if (!isNil(currentData) && currentData.length === 20) { + return allPages.length * 20; + } + return undefined; + }, + }); +} diff --git a/client/cms/src/lib/navData.ts b/client/cms/src/lib/navData.ts index f5f24ad..3408702 100644 --- a/client/cms/src/lib/navData.ts +++ b/client/cms/src/lib/navData.ts @@ -1,4 +1,5 @@ import { + IconCalendarClock, IconCalendarEvent, IconDashboard, IconUser, @@ -16,6 +17,11 @@ export const navData = { url: '/events', icon: IconCalendarEvent, }, + { + title: '已加入的活动', + url: '/joined-events', + icon: IconCalendarClock, + }, ], navSecondary: [ { diff --git a/client/cms/src/lib/utils.ts b/client/cms/src/lib/utils.ts index 9c08cf1..73a19e9 100644 --- a/client/cms/src/lib/utils.ts +++ b/client/cms/src/lib/utils.ts @@ -27,3 +27,8 @@ export function invalidateBlurry(id: string) { }, }; } + +export function isInDateRange(start: Date, end: Date, target: Date = new Date()): boolean { + const time = target.getTime(); + return time >= start.getTime() && time <= end.getTime(); +} diff --git a/client/cms/src/routeTree.gen.ts b/client/cms/src/routeTree.gen.ts index 26aa039..723a86c 100644 --- a/client/cms/src/routeTree.gen.ts +++ b/client/cms/src/routeTree.gen.ts @@ -14,9 +14,12 @@ import { Route as MagicLinkSentRouteImport } from './routes/magicLinkSent' import { Route as AuthorizeRouteImport } from './routes/authorize' import { Route as WorkbenchLayoutRouteImport } from './routes/_workbenchLayout' import { Route as WorkbenchLayoutIndexRouteImport } from './routes/_workbenchLayout/index' +import { Route as WorkbenchLayoutJoinedEventsRouteImport } from './routes/_workbenchLayout/joined-events' import { Route as WorkbenchLayoutEventsRouteImport } from './routes/_workbenchLayout/events' -import { Route as WorkbenchLayoutProfileIndexRouteImport } from './routes/_workbenchLayout/profile.index' -import { Route as WorkbenchLayoutProfileUserIdRouteImport } from './routes/_workbenchLayout/profile.$userId' +import { Route as WorkbenchLayoutProfileIndexRouteImport } from './routes/_workbenchLayout/profile/index' +import { Route as WorkbenchLayoutEventsIndexRouteImport } from './routes/_workbenchLayout/events/index' +import { Route as WorkbenchLayoutProfileUserIdRouteImport } from './routes/_workbenchLayout/profile/$userId' +import { Route as WorkbenchLayoutEventsEventIdRouteImport } from './routes/_workbenchLayout/events/$eventId' const TokenRoute = TokenRouteImport.update({ id: '/token', @@ -42,6 +45,12 @@ const WorkbenchLayoutIndexRoute = WorkbenchLayoutIndexRouteImport.update({ path: '/', getParentRoute: () => WorkbenchLayoutRoute, } as any) +const WorkbenchLayoutJoinedEventsRoute = + WorkbenchLayoutJoinedEventsRouteImport.update({ + id: '/joined-events', + path: '/joined-events', + getParentRoute: () => WorkbenchLayoutRoute, + } as any) const WorkbenchLayoutEventsRoute = WorkbenchLayoutEventsRouteImport.update({ id: '/events', path: '/events', @@ -53,29 +62,46 @@ const WorkbenchLayoutProfileIndexRoute = path: '/profile/', getParentRoute: () => WorkbenchLayoutRoute, } as any) +const WorkbenchLayoutEventsIndexRoute = + WorkbenchLayoutEventsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => WorkbenchLayoutEventsRoute, + } as any) const WorkbenchLayoutProfileUserIdRoute = WorkbenchLayoutProfileUserIdRouteImport.update({ id: '/profile/$userId', path: '/profile/$userId', getParentRoute: () => WorkbenchLayoutRoute, } as any) +const WorkbenchLayoutEventsEventIdRoute = + WorkbenchLayoutEventsEventIdRouteImport.update({ + id: '/$eventId', + path: '/$eventId', + getParentRoute: () => WorkbenchLayoutEventsRoute, + } as any) export interface FileRoutesByFullPath { '/': typeof WorkbenchLayoutIndexRoute '/authorize': typeof AuthorizeRoute '/magicLinkSent': typeof MagicLinkSentRoute '/token': typeof TokenRoute - '/events': typeof WorkbenchLayoutEventsRoute + '/events': typeof WorkbenchLayoutEventsRouteWithChildren + '/joined-events': typeof WorkbenchLayoutJoinedEventsRoute + '/events/$eventId': typeof WorkbenchLayoutEventsEventIdRoute '/profile/$userId': typeof WorkbenchLayoutProfileUserIdRoute + '/events/': typeof WorkbenchLayoutEventsIndexRoute '/profile/': typeof WorkbenchLayoutProfileIndexRoute } export interface FileRoutesByTo { '/authorize': typeof AuthorizeRoute '/magicLinkSent': typeof MagicLinkSentRoute '/token': typeof TokenRoute - '/events': typeof WorkbenchLayoutEventsRoute + '/joined-events': typeof WorkbenchLayoutJoinedEventsRoute '/': typeof WorkbenchLayoutIndexRoute + '/events/$eventId': typeof WorkbenchLayoutEventsEventIdRoute '/profile/$userId': typeof WorkbenchLayoutProfileUserIdRoute + '/events': typeof WorkbenchLayoutEventsIndexRoute '/profile': typeof WorkbenchLayoutProfileIndexRoute } export interface FileRoutesById { @@ -84,9 +110,12 @@ export interface FileRoutesById { '/authorize': typeof AuthorizeRoute '/magicLinkSent': typeof MagicLinkSentRoute '/token': typeof TokenRoute - '/_workbenchLayout/events': typeof WorkbenchLayoutEventsRoute + '/_workbenchLayout/events': typeof WorkbenchLayoutEventsRouteWithChildren + '/_workbenchLayout/joined-events': typeof WorkbenchLayoutJoinedEventsRoute '/_workbenchLayout/': typeof WorkbenchLayoutIndexRoute + '/_workbenchLayout/events/$eventId': typeof WorkbenchLayoutEventsEventIdRoute '/_workbenchLayout/profile/$userId': typeof WorkbenchLayoutProfileUserIdRoute + '/_workbenchLayout/events/': typeof WorkbenchLayoutEventsIndexRoute '/_workbenchLayout/profile/': typeof WorkbenchLayoutProfileIndexRoute } export interface FileRouteTypes { @@ -97,16 +126,21 @@ export interface FileRouteTypes { | '/magicLinkSent' | '/token' | '/events' + | '/joined-events' + | '/events/$eventId' | '/profile/$userId' + | '/events/' | '/profile/' fileRoutesByTo: FileRoutesByTo to: | '/authorize' | '/magicLinkSent' | '/token' - | '/events' + | '/joined-events' | '/' + | '/events/$eventId' | '/profile/$userId' + | '/events' | '/profile' id: | '__root__' @@ -115,8 +149,11 @@ export interface FileRouteTypes { | '/magicLinkSent' | '/token' | '/_workbenchLayout/events' + | '/_workbenchLayout/joined-events' | '/_workbenchLayout/' + | '/_workbenchLayout/events/$eventId' | '/_workbenchLayout/profile/$userId' + | '/_workbenchLayout/events/' | '/_workbenchLayout/profile/' fileRoutesById: FileRoutesById } @@ -164,6 +201,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof WorkbenchLayoutIndexRouteImport parentRoute: typeof WorkbenchLayoutRoute } + '/_workbenchLayout/joined-events': { + id: '/_workbenchLayout/joined-events' + path: '/joined-events' + fullPath: '/joined-events' + preLoaderRoute: typeof WorkbenchLayoutJoinedEventsRouteImport + parentRoute: typeof WorkbenchLayoutRoute + } '/_workbenchLayout/events': { id: '/_workbenchLayout/events' path: '/events' @@ -178,6 +222,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof WorkbenchLayoutProfileIndexRouteImport parentRoute: typeof WorkbenchLayoutRoute } + '/_workbenchLayout/events/': { + id: '/_workbenchLayout/events/' + path: '/' + fullPath: '/events/' + preLoaderRoute: typeof WorkbenchLayoutEventsIndexRouteImport + parentRoute: typeof WorkbenchLayoutEventsRoute + } '/_workbenchLayout/profile/$userId': { id: '/_workbenchLayout/profile/$userId' path: '/profile/$userId' @@ -185,18 +236,42 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof WorkbenchLayoutProfileUserIdRouteImport parentRoute: typeof WorkbenchLayoutRoute } + '/_workbenchLayout/events/$eventId': { + id: '/_workbenchLayout/events/$eventId' + path: '/$eventId' + fullPath: '/events/$eventId' + preLoaderRoute: typeof WorkbenchLayoutEventsEventIdRouteImport + parentRoute: typeof WorkbenchLayoutEventsRoute + } } } +interface WorkbenchLayoutEventsRouteChildren { + WorkbenchLayoutEventsEventIdRoute: typeof WorkbenchLayoutEventsEventIdRoute + WorkbenchLayoutEventsIndexRoute: typeof WorkbenchLayoutEventsIndexRoute +} + +const WorkbenchLayoutEventsRouteChildren: WorkbenchLayoutEventsRouteChildren = { + WorkbenchLayoutEventsEventIdRoute: WorkbenchLayoutEventsEventIdRoute, + WorkbenchLayoutEventsIndexRoute: WorkbenchLayoutEventsIndexRoute, +} + +const WorkbenchLayoutEventsRouteWithChildren = + WorkbenchLayoutEventsRoute._addFileChildren( + WorkbenchLayoutEventsRouteChildren, + ) + interface WorkbenchLayoutRouteChildren { - WorkbenchLayoutEventsRoute: typeof WorkbenchLayoutEventsRoute + WorkbenchLayoutEventsRoute: typeof WorkbenchLayoutEventsRouteWithChildren + WorkbenchLayoutJoinedEventsRoute: typeof WorkbenchLayoutJoinedEventsRoute WorkbenchLayoutIndexRoute: typeof WorkbenchLayoutIndexRoute WorkbenchLayoutProfileUserIdRoute: typeof WorkbenchLayoutProfileUserIdRoute WorkbenchLayoutProfileIndexRoute: typeof WorkbenchLayoutProfileIndexRoute } const WorkbenchLayoutRouteChildren: WorkbenchLayoutRouteChildren = { - WorkbenchLayoutEventsRoute: WorkbenchLayoutEventsRoute, + WorkbenchLayoutEventsRoute: WorkbenchLayoutEventsRouteWithChildren, + WorkbenchLayoutJoinedEventsRoute: WorkbenchLayoutJoinedEventsRoute, WorkbenchLayoutIndexRoute: WorkbenchLayoutIndexRoute, WorkbenchLayoutProfileUserIdRoute: WorkbenchLayoutProfileUserIdRoute, WorkbenchLayoutProfileIndexRoute: WorkbenchLayoutProfileIndexRoute, diff --git a/client/cms/src/routes/_workbenchLayout/events/$eventId.tsx b/client/cms/src/routes/_workbenchLayout/events/$eventId.tsx new file mode 100644 index 0000000..e032440 --- /dev/null +++ b/client/cms/src/routes/_workbenchLayout/events/$eventId.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/_workbenchLayout/events/$eventId')({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/_workbenchLayout/events/$eventId"!
; +} diff --git a/client/cms/src/routes/_workbenchLayout/events/index.tsx b/client/cms/src/routes/_workbenchLayout/events/index.tsx new file mode 100644 index 0000000..d5db551 --- /dev/null +++ b/client/cms/src/routes/_workbenchLayout/events/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { EventGridContainer } from '@/components/events/event-grid/event-grid.container'; + +export const Route = createFileRoute('/_workbenchLayout/events/')({ + component: RouteComponent, +}); + +function RouteComponent() { + return ( +
+ +
+ ); +} diff --git a/client/cms/src/routes/_workbenchLayout/joined-events.tsx b/client/cms/src/routes/_workbenchLayout/joined-events.tsx new file mode 100644 index 0000000..21d995a --- /dev/null +++ b/client/cms/src/routes/_workbenchLayout/joined-events.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { JoinedEventsContainer } from '@/components/events/joined-events.containers'; + +export const Route = createFileRoute('/_workbenchLayout/joined-events')({ + component: RouteComponent, +}); + +function RouteComponent() { + return ( +
+ +
+ ); +} diff --git a/client/cms/src/routes/_workbenchLayout/profile.$userId.tsx b/client/cms/src/routes/_workbenchLayout/profile/$userId.tsx similarity index 100% rename from client/cms/src/routes/_workbenchLayout/profile.$userId.tsx rename to client/cms/src/routes/_workbenchLayout/profile/$userId.tsx diff --git a/client/cms/src/routes/_workbenchLayout/profile.index.tsx b/client/cms/src/routes/_workbenchLayout/profile/index.tsx similarity index 100% rename from client/cms/src/routes/_workbenchLayout/profile.index.tsx rename to client/cms/src/routes/_workbenchLayout/profile/index.tsx diff --git a/client/cms/src/stories/events/event-grid.stories.tsx b/client/cms/src/stories/events/event-grid.stories.tsx index 82c3607..290130b 100644 --- a/client/cms/src/stories/events/event-grid.stories.tsx +++ b/client/cms/src/stories/events/event-grid.stories.tsx @@ -1,8 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { EventGridSkeleton } from '@/components/events/event-grid/event-grid.skeleton'; import { EventGridView } from '@/components/events/event-grid/event-grid.view'; +import { JoinedEventGridFooter } from '@/components/events/joined-events.containers'; import { Button } from '@/components/ui/button'; import { Skeleton as UiSkeleton } from '@/components/ui/skeleton'; +import { exampleMultiEvents } from './event.example'; const meta = { title: 'Events/EventGrid', @@ -14,56 +16,18 @@ type Story = StoryObj; export const Primary: Story = { args: { - events: [ - { - eventId: '1', - requireKyc: true, - isJoined: false, - type: 'official', - coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-watersplash.png?raw=true', - eventName: 'Nix CN Conference 26.05', - description: 'Event Description', - startTime: new Date('2026-06-13T04:00:00.000Z'), - endTime: new Date('2026-06-14T04:00:00.000Z'), - }, - { - eventId: '2', - requireKyc: true, - isJoined: false, - type: 'official', - coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-moonscape.png?raw=true', - eventName: 'Nix CN Conference 26.05', - description: 'Event Description', - startTime: new Date('2026-06-13T04:00:00.000Z'), - endTime: new Date('2026-06-14T04:00:00.000Z'), - }, - { - eventId: '3', - requireKyc: true, - isJoined: false, - type: 'official', - coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-nineish-catppuccin-latte.png?raw=true', - eventName: 'Nix CN Conference 26.05', - description: 'Event Description', - startTime: new Date('2026-06-13T04:00:00.000Z'), - endTime: new Date('2026-06-14T04:00:00.000Z'), - }, - { - eventId: '4', - requireKyc: true, - isJoined: false, - type: 'official', - coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nixos-wallpaper-catppuccin-macchiato.png?raw=true', - eventName: 'Nix CN Conference 26.05', - description: 'Event Description', - startTime: new Date('2026-06-13T04:00:00.000Z'), - endTime: new Date('2026-06-14T04:00:00.000Z'), - }, - ], + events: exampleMultiEvents, footer: () => , }, }; +export const Joined: Story = { + args: { + events: exampleMultiEvents, + footer: event => , + }, +}; + export const Empty: Story = { decorators: [Story =>
], args: { diff --git a/client/cms/src/stories/events/event.example.ts b/client/cms/src/stories/events/event.example.ts index 1dc8ec5..04001e6 100644 --- a/client/cms/src/stories/events/event.example.ts +++ b/client/cms/src/stories/events/event.example.ts @@ -11,3 +11,50 @@ export const exampleEvent: EventInfo = { startTime: new Date('2026-06-13T04:00:00.000Z'), endTime: new Date('2026-06-14T04:00:00.000Z'), }; + +export const exampleMultiEvents: EventInfo[] = [ + { + eventId: '1', + requireKyc: true, + isJoined: false, + type: 'official', + coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-watersplash.png?raw=true', + eventName: 'Nix CN Conference 26.05', + description: 'Event Description', + startTime: new Date('2026-06-13T04:00:00.000Z'), + endTime: new Date('2026-06-14T04:00:00.000Z'), + }, + { + eventId: '2', + requireKyc: true, + isJoined: false, + type: 'official', + coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-moonscape.png?raw=true', + eventName: 'Nix CN Conference 26.05', + description: 'Event Description', + startTime: new Date('2026-06-13T04:00:00.000Z'), + endTime: new Date('2026-06-14T04:00:00.000Z'), + }, + { + eventId: '3', + requireKyc: true, + isJoined: false, + type: 'official', + coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-nineish-catppuccin-latte.png?raw=true', + eventName: 'Nix CN Conference 26.05', + description: 'Event Description', + startTime: new Date('2026-06-13T04:00:00.000Z'), + endTime: new Date('2026-06-14T04:00:00.000Z'), + }, + { + eventId: '4', + requireKyc: true, + isJoined: false, + type: 'official', + coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nixos-wallpaper-catppuccin-macchiato.png?raw=true', + eventName: 'Nix CN Conference 26.05', + description: 'Event Description', + startTime: new Date('2026-06-13T04:00:00.000Z'), + endTime: new Date('2026-06-14T04:00:00.000Z'), + }, +];