{user.nickname}
diff --git a/client/cms/src/components/workbenchCards/event-card.tsx b/client/cms/src/components/workbenchCards/event-card.tsx
new file mode 100644
index 0000000..2820413
--- /dev/null
+++ b/client/cms/src/components/workbenchCards/event-card.tsx
@@ -0,0 +1,39 @@
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import {
+ Card,
+ CardAction,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card"
+export function EventCard({ type, coverImage, eventName, description, startTime, endTime }:
+ {
+ type: 'official' | 'party',
+ coverImage: string, eventName: string, description: string, startTime: string, endTime: string
+ }) {
+ return (
+
+
+
+
+
+ Featured
+
+ Design systems meetup
+
+ A practical talk on component APIs, accessibility, and shipping
+ faster.
+
+
+
+
+
+
+ )
+}
diff --git a/client/cms/src/hooks/data/useUserInfo.ts b/client/cms/src/hooks/data/useUserInfo.ts
index db64bd6..369b0ee 100644
--- a/client/cms/src/hooks/data/useUserInfo.ts
+++ b/client/cms/src/hooks/data/useUserInfo.ts
@@ -1,5 +1,8 @@
import { useSuspenseQuery } from '@tanstack/react-query';
-import { getUserInfoOptions } from '@/client/@tanstack/react-query.gen';
+import {
+ getUserInfoByUserIdOptions,
+ getUserInfoOptions,
+} from '@/client/@tanstack/react-query.gen';
export function useUserInfo() {
return useSuspenseQuery({
@@ -7,3 +10,11 @@ export function useUserInfo() {
staleTime: 10 * 60 * 1000,
});
}
+
+export function useOtherUserInfo(userId: string) {
+ return useSuspenseQuery({
+ ...getUserInfoByUserIdOptions({ path: { user_id: userId } }),
+ staleTime: 10 * 60 * 1000,
+ retry: (_failureCount, error) => error.code !== 403,
+ });
+}
diff --git a/client/cms/src/lib/navData.ts b/client/cms/src/lib/navData.ts
index 7266d47..83be1ad 100644
--- a/client/cms/src/lib/navData.ts
+++ b/client/cms/src/lib/navData.ts
@@ -1,4 +1,5 @@
import {
+ IconCalendarEvent,
IconDashboard,
IconUser,
} from '@tabler/icons-react';
@@ -10,6 +11,11 @@ export const navData = {
url: '/',
icon: IconDashboard,
},
+ {
+ title: '活动列表',
+ url: '/events',
+ icon: IconCalendarEvent,
+ },
],
navSecondary: [
{
diff --git a/client/cms/src/routeTree.gen.ts b/client/cms/src/routeTree.gen.ts
index 7d1304c..26aa039 100644
--- a/client/cms/src/routeTree.gen.ts
+++ b/client/cms/src/routeTree.gen.ts
@@ -12,9 +12,11 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as TokenRouteImport } from './routes/token'
import { Route as MagicLinkSentRouteImport } from './routes/magicLinkSent'
import { Route as AuthorizeRouteImport } from './routes/authorize'
-import { Route as SidebarLayoutRouteImport } from './routes/_sidebarLayout'
-import { Route as SidebarLayoutIndexRouteImport } from './routes/_sidebarLayout/index'
-import { Route as SidebarLayoutProfileRouteImport } from './routes/_sidebarLayout/profile'
+import { Route as WorkbenchLayoutRouteImport } from './routes/_workbenchLayout'
+import { Route as WorkbenchLayoutIndexRouteImport } from './routes/_workbenchLayout/index'
+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'
const TokenRoute = TokenRouteImport.update({
id: '/token',
@@ -31,61 +33,95 @@ const AuthorizeRoute = AuthorizeRouteImport.update({
path: '/authorize',
getParentRoute: () => rootRouteImport,
} as any)
-const SidebarLayoutRoute = SidebarLayoutRouteImport.update({
- id: '/_sidebarLayout',
+const WorkbenchLayoutRoute = WorkbenchLayoutRouteImport.update({
+ id: '/_workbenchLayout',
getParentRoute: () => rootRouteImport,
} as any)
-const SidebarLayoutIndexRoute = SidebarLayoutIndexRouteImport.update({
+const WorkbenchLayoutIndexRoute = WorkbenchLayoutIndexRouteImport.update({
id: '/',
path: '/',
- getParentRoute: () => SidebarLayoutRoute,
+ getParentRoute: () => WorkbenchLayoutRoute,
} as any)
-const SidebarLayoutProfileRoute = SidebarLayoutProfileRouteImport.update({
- id: '/profile',
- path: '/profile',
- getParentRoute: () => SidebarLayoutRoute,
+const WorkbenchLayoutEventsRoute = WorkbenchLayoutEventsRouteImport.update({
+ id: '/events',
+ path: '/events',
+ getParentRoute: () => WorkbenchLayoutRoute,
} as any)
+const WorkbenchLayoutProfileIndexRoute =
+ WorkbenchLayoutProfileIndexRouteImport.update({
+ id: '/profile/',
+ path: '/profile/',
+ getParentRoute: () => WorkbenchLayoutRoute,
+ } as any)
+const WorkbenchLayoutProfileUserIdRoute =
+ WorkbenchLayoutProfileUserIdRouteImport.update({
+ id: '/profile/$userId',
+ path: '/profile/$userId',
+ getParentRoute: () => WorkbenchLayoutRoute,
+ } as any)
export interface FileRoutesByFullPath {
- '/': typeof SidebarLayoutIndexRoute
+ '/': typeof WorkbenchLayoutIndexRoute
'/authorize': typeof AuthorizeRoute
'/magicLinkSent': typeof MagicLinkSentRoute
'/token': typeof TokenRoute
- '/profile': typeof SidebarLayoutProfileRoute
+ '/events': typeof WorkbenchLayoutEventsRoute
+ '/profile/$userId': typeof WorkbenchLayoutProfileUserIdRoute
+ '/profile/': typeof WorkbenchLayoutProfileIndexRoute
}
export interface FileRoutesByTo {
'/authorize': typeof AuthorizeRoute
'/magicLinkSent': typeof MagicLinkSentRoute
'/token': typeof TokenRoute
- '/profile': typeof SidebarLayoutProfileRoute
- '/': typeof SidebarLayoutIndexRoute
+ '/events': typeof WorkbenchLayoutEventsRoute
+ '/': typeof WorkbenchLayoutIndexRoute
+ '/profile/$userId': typeof WorkbenchLayoutProfileUserIdRoute
+ '/profile': typeof WorkbenchLayoutProfileIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
- '/_sidebarLayout': typeof SidebarLayoutRouteWithChildren
+ '/_workbenchLayout': typeof WorkbenchLayoutRouteWithChildren
'/authorize': typeof AuthorizeRoute
'/magicLinkSent': typeof MagicLinkSentRoute
'/token': typeof TokenRoute
- '/_sidebarLayout/profile': typeof SidebarLayoutProfileRoute
- '/_sidebarLayout/': typeof SidebarLayoutIndexRoute
+ '/_workbenchLayout/events': typeof WorkbenchLayoutEventsRoute
+ '/_workbenchLayout/': typeof WorkbenchLayoutIndexRoute
+ '/_workbenchLayout/profile/$userId': typeof WorkbenchLayoutProfileUserIdRoute
+ '/_workbenchLayout/profile/': typeof WorkbenchLayoutProfileIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
- fullPaths: '/' | '/authorize' | '/magicLinkSent' | '/token' | '/profile'
- fileRoutesByTo: FileRoutesByTo
- to: '/authorize' | '/magicLinkSent' | '/token' | '/profile' | '/'
- id:
- | '__root__'
- | '/_sidebarLayout'
+ fullPaths:
+ | '/'
| '/authorize'
| '/magicLinkSent'
| '/token'
- | '/_sidebarLayout/profile'
- | '/_sidebarLayout/'
+ | '/events'
+ | '/profile/$userId'
+ | '/profile/'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/authorize'
+ | '/magicLinkSent'
+ | '/token'
+ | '/events'
+ | '/'
+ | '/profile/$userId'
+ | '/profile'
+ id:
+ | '__root__'
+ | '/_workbenchLayout'
+ | '/authorize'
+ | '/magicLinkSent'
+ | '/token'
+ | '/_workbenchLayout/events'
+ | '/_workbenchLayout/'
+ | '/_workbenchLayout/profile/$userId'
+ | '/_workbenchLayout/profile/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
- SidebarLayoutRoute: typeof SidebarLayoutRouteWithChildren
+ WorkbenchLayoutRoute: typeof WorkbenchLayoutRouteWithChildren
AuthorizeRoute: typeof AuthorizeRoute
MagicLinkSentRoute: typeof MagicLinkSentRoute
TokenRoute: typeof TokenRoute
@@ -114,46 +150,64 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthorizeRouteImport
parentRoute: typeof rootRouteImport
}
- '/_sidebarLayout': {
- id: '/_sidebarLayout'
+ '/_workbenchLayout': {
+ id: '/_workbenchLayout'
path: ''
fullPath: '/'
- preLoaderRoute: typeof SidebarLayoutRouteImport
+ preLoaderRoute: typeof WorkbenchLayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/_sidebarLayout/': {
- id: '/_sidebarLayout/'
+ '/_workbenchLayout/': {
+ id: '/_workbenchLayout/'
path: '/'
fullPath: '/'
- preLoaderRoute: typeof SidebarLayoutIndexRouteImport
- parentRoute: typeof SidebarLayoutRoute
+ preLoaderRoute: typeof WorkbenchLayoutIndexRouteImport
+ parentRoute: typeof WorkbenchLayoutRoute
}
- '/_sidebarLayout/profile': {
- id: '/_sidebarLayout/profile'
+ '/_workbenchLayout/events': {
+ id: '/_workbenchLayout/events'
+ path: '/events'
+ fullPath: '/events'
+ preLoaderRoute: typeof WorkbenchLayoutEventsRouteImport
+ parentRoute: typeof WorkbenchLayoutRoute
+ }
+ '/_workbenchLayout/profile/': {
+ id: '/_workbenchLayout/profile/'
path: '/profile'
- fullPath: '/profile'
- preLoaderRoute: typeof SidebarLayoutProfileRouteImport
- parentRoute: typeof SidebarLayoutRoute
+ fullPath: '/profile/'
+ preLoaderRoute: typeof WorkbenchLayoutProfileIndexRouteImport
+ parentRoute: typeof WorkbenchLayoutRoute
+ }
+ '/_workbenchLayout/profile/$userId': {
+ id: '/_workbenchLayout/profile/$userId'
+ path: '/profile/$userId'
+ fullPath: '/profile/$userId'
+ preLoaderRoute: typeof WorkbenchLayoutProfileUserIdRouteImport
+ parentRoute: typeof WorkbenchLayoutRoute
}
}
}
-interface SidebarLayoutRouteChildren {
- SidebarLayoutProfileRoute: typeof SidebarLayoutProfileRoute
- SidebarLayoutIndexRoute: typeof SidebarLayoutIndexRoute
+interface WorkbenchLayoutRouteChildren {
+ WorkbenchLayoutEventsRoute: typeof WorkbenchLayoutEventsRoute
+ WorkbenchLayoutIndexRoute: typeof WorkbenchLayoutIndexRoute
+ WorkbenchLayoutProfileUserIdRoute: typeof WorkbenchLayoutProfileUserIdRoute
+ WorkbenchLayoutProfileIndexRoute: typeof WorkbenchLayoutProfileIndexRoute
}
-const SidebarLayoutRouteChildren: SidebarLayoutRouteChildren = {
- SidebarLayoutProfileRoute: SidebarLayoutProfileRoute,
- SidebarLayoutIndexRoute: SidebarLayoutIndexRoute,
+const WorkbenchLayoutRouteChildren: WorkbenchLayoutRouteChildren = {
+ WorkbenchLayoutEventsRoute: WorkbenchLayoutEventsRoute,
+ WorkbenchLayoutIndexRoute: WorkbenchLayoutIndexRoute,
+ WorkbenchLayoutProfileUserIdRoute: WorkbenchLayoutProfileUserIdRoute,
+ WorkbenchLayoutProfileIndexRoute: WorkbenchLayoutProfileIndexRoute,
}
-const SidebarLayoutRouteWithChildren = SidebarLayoutRoute._addFileChildren(
- SidebarLayoutRouteChildren,
+const WorkbenchLayoutRouteWithChildren = WorkbenchLayoutRoute._addFileChildren(
+ WorkbenchLayoutRouteChildren,
)
const rootRouteChildren: RootRouteChildren = {
- SidebarLayoutRoute: SidebarLayoutRouteWithChildren,
+ WorkbenchLayoutRoute: WorkbenchLayoutRouteWithChildren,
AuthorizeRoute: AuthorizeRoute,
MagicLinkSentRoute: MagicLinkSentRoute,
TokenRoute: TokenRoute,
diff --git a/client/cms/src/routes/_sidebarLayout/profile.tsx b/client/cms/src/routes/_sidebarLayout/profile.tsx
deleted file mode 100644
index 7dfcf31..0000000
--- a/client/cms/src/routes/_sidebarLayout/profile.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createFileRoute } from '@tanstack/react-router';
-import { MainProfile } from '@/components/profile/main-profile';
-
-export const Route = createFileRoute('/_sidebarLayout/profile')({
- component: RouteComponent,
-});
-
-function RouteComponent() {
- return (
-
-
-
- );
-}
diff --git a/client/cms/src/routes/_sidebarLayout.tsx b/client/cms/src/routes/_workbenchLayout.tsx
similarity index 93%
rename from client/cms/src/routes/_sidebarLayout.tsx
rename to client/cms/src/routes/_workbenchLayout.tsx
index e282a5e..a52b375 100644
--- a/client/cms/src/routes/_sidebarLayout.tsx
+++ b/client/cms/src/routes/_workbenchLayout.tsx
@@ -3,7 +3,7 @@ import { AppSidebar } from '@/components/sidebar/app-sidebar';
import { SiteHeader } from '@/components/site-header';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
-export const Route = createFileRoute('/_sidebarLayout')({
+export const Route = createFileRoute('/_workbenchLayout')({
component: RouteComponent,
});
diff --git a/client/cms/src/routes/_workbenchLayout/events.tsx b/client/cms/src/routes/_workbenchLayout/events.tsx
new file mode 100644
index 0000000..22bd0b1
--- /dev/null
+++ b/client/cms/src/routes/_workbenchLayout/events.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router';
+
+export const Route = createFileRoute('/_workbenchLayout/events')({
+ component: RouteComponent,
+});
+
+function RouteComponent() {
+ return
Hello "/_sidebarLayout/events"!
;
+}
diff --git a/client/cms/src/routes/_sidebarLayout/index.tsx b/client/cms/src/routes/_workbenchLayout/index.tsx
similarity index 90%
rename from client/cms/src/routes/_sidebarLayout/index.tsx
rename to client/cms/src/routes/_workbenchLayout/index.tsx
index 023f537..c16b887 100644
--- a/client/cms/src/routes/_sidebarLayout/index.tsx
+++ b/client/cms/src/routes/_workbenchLayout/index.tsx
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
-export const Route = createFileRoute('/_sidebarLayout/')({
+export const Route = createFileRoute('/_workbenchLayout/')({
component: Index,
});
diff --git a/client/cms/src/routes/_workbenchLayout/profile.$userId.tsx b/client/cms/src/routes/_workbenchLayout/profile.$userId.tsx
new file mode 100644
index 0000000..5f18e4a
--- /dev/null
+++ b/client/cms/src/routes/_workbenchLayout/profile.$userId.tsx
@@ -0,0 +1,34 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { Suspense } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+import { Profile } from '@/components/profile/profile';
+import { ProfileError } from '@/components/profile/profile.error';
+import { ProfileSkeleton } from '@/components/profile/profile.skeleton';
+import { useOtherUserInfo } from '@/hooks/data/useUserInfo';
+
+export const Route = createFileRoute('/_workbenchLayout/profile/$userId')({
+ component: RouteComponent,
+});
+
+function ProfileByUserId({ userId }: { userId: string }) {
+ const { data } = useOtherUserInfo(userId);
+ return
;
+}
+
+function RouteComponent() {
+ const { userId } = Route.useParams();
+ return (
+
+
{
+ if ((error.error as { code: number }).code === 403)
+ return ;
+ else return ;
+ }}
+ >
+ }>
+
+
+
+
+ );
+}
diff --git a/client/cms/src/routes/_workbenchLayout/profile.index.tsx b/client/cms/src/routes/_workbenchLayout/profile.index.tsx
new file mode 100644
index 0000000..090e248
--- /dev/null
+++ b/client/cms/src/routes/_workbenchLayout/profile.index.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute, Navigate } from '@tanstack/react-router';
+import { useUserInfo } from '@/hooks/data/useUserInfo';
+
+export const Route = createFileRoute('/_workbenchLayout/profile/')({
+ component: RouteComponent,
+});
+
+function RouteComponent() {
+ const { data } = useUserInfo();
+ return (
+
+ );
+}
diff --git a/client/cms/src/stories/event-card.stories.ts b/client/cms/src/stories/event-card.stories.ts
new file mode 100644
index 0000000..262abff
--- /dev/null
+++ b/client/cms/src/stories/event-card.stories.ts
@@ -0,0 +1,12 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { EventCard } from '@/components/workbenchCards/event-card';
+
+const meta = {
+ title: 'Cards/EventCard',
+ component: EventCard,
+} satisfies Meta
;
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {};
diff --git a/client/cms/src/stories/profile.stories.tsx b/client/cms/src/stories/profile.stories.tsx
new file mode 100644
index 0000000..c4e6152
--- /dev/null
+++ b/client/cms/src/stories/profile.stories.tsx
@@ -0,0 +1,51 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { Profile } from '@/components/profile/profile';
+import { ProfileError } from '@/components/profile/profile.error';
+import { ProfileSkeleton } from '@/components/profile/profile.skeleton';
+
+const queryClient = new QueryClient();
+
+const meta = {
+ title: 'Profile',
+ component: Profile,
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ user: {
+ username: 'nvirellia',
+ nickname: 'Noa Virellia',
+ subtitle: '天生骄傲',
+ email: 'noa@requiem.garden',
+ bio: '',
+ avatar: 'https://avatars.githubusercontent.com/u/54884471?v=4',
+ },
+ },
+};
+
+export const Skeleton: Story = {
+ render: () => ,
+ args: {
+ user: {},
+ },
+};
+
+export const Error: Story = {
+ render: () => ,
+ args: {
+ user: {
+ allow_public: false,
+ },
+ },
+};
diff --git a/client/cms/vite.config.ts b/client/cms/vite.config.ts
index 06cd972..f4dea43 100644
--- a/client/cms/vite.config.ts
+++ b/client/cms/vite.config.ts
@@ -1,3 +1,4 @@
+///
import path from 'node:path';
import tailwindcss from '@tailwindcss/vite';
import { tanstackRouter } from '@tanstack/router-plugin/vite';
@@ -6,27 +7,51 @@ import { defineConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
// https://vite.dev/config/
+import { fileURLToPath } from 'node:url';
+import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+import { playwright } from '@vitest/browser-playwright';
+const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+
+// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
export default defineConfig({
- plugins: [
- tanstackRouter({
- target: 'react',
- autoCodeSplitting: true,
- }),
- react(),
- tailwindcss(),
- svgr(),
- ],
+ plugins: [tanstackRouter({
+ target: 'react',
+ autoCodeSplitting: true
+ }), react(), tailwindcss(), svgr()],
resolve: {
alias: {
- '@': path.resolve(__dirname, './src'),
- },
+ '@': path.resolve(__dirname, './src')
+ }
},
server: {
proxy: {
- '/api': 'http://10.0.0.10:8000',
+ '/api': 'http://10.0.0.10:8000'
},
host: '0.0.0.0',
port: 5173,
- allowedHosts: ['nix.org.cn', 'nixos.party'],
+ allowedHosts: ['nix.org.cn', 'nixos.party']
},
-});
+ test: {
+ projects: [{
+ extends: true,
+ plugins: [
+ // The plugin will run tests for the stories defined in your Storybook config
+ // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ storybookTest({
+ configDir: path.join(dirname, '.storybook')
+ })],
+ test: {
+ name: 'storybook',
+ browser: {
+ enabled: true,
+ headless: true,
+ provider: playwright({}),
+ instances: [{
+ browser: 'chromium'
+ }]
+ },
+ setupFiles: ['.storybook/vitest.setup.ts']
+ }
+ }]
+ }
+});
\ No newline at end of file
diff --git a/client/cms/vitest.shims.d.ts b/client/cms/vitest.shims.d.ts
new file mode 100644
index 0000000..7782f28
--- /dev/null
+++ b/client/cms/vitest.shims.d.ts
@@ -0,0 +1 @@
+///
\ No newline at end of file