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'),
+ },
+];