refactor(sidebar): split nav views and add router decorator
Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
@@ -1,9 +1,21 @@
|
|||||||
import type { Preview } from '@storybook/react-vite';
|
import type { Decorator, Preview } from '@storybook/react-vite';
|
||||||
import { ThemeProvider } from '../src/components/theme-provider';
|
import { ThemeProvider } from '../src/components/theme-provider';
|
||||||
import '../src/index.css';
|
import '../src/index.css';
|
||||||
|
import { createRootRoute, createRouter, RouterProvider } from '@tanstack/react-router';
|
||||||
|
|
||||||
|
const RouterDecorator: Decorator = (Story) => {
|
||||||
|
const rootRoute = createRootRoute({ component: () => <Story /> });
|
||||||
|
const routeTree = rootRoute;
|
||||||
|
const router = createRouter({ routeTree });
|
||||||
|
return <RouterProvider router={router} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThemeDecorator: Decorator = (Story) => {
|
||||||
|
return <ThemeProvider defaultTheme="dark"><Story /></ThemeProvider>;
|
||||||
|
};
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
decorators: [(Story) => <ThemeProvider defaultTheme="dark"><Story /></ThemeProvider >],
|
decorators: [RouterDecorator, ThemeDecorator],
|
||||||
parameters: {
|
parameters: {
|
||||||
controls: {
|
controls: {
|
||||||
matchers: {
|
matchers: {
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import type { ReactNode } from 'react';
|
|
||||||
import React, { Suspense } from 'react';
|
|
||||||
|
|
||||||
export function withFallback<P extends object>(
|
|
||||||
Component: React.ComponentType<P>,
|
|
||||||
fallback: ReactNode,
|
|
||||||
) {
|
|
||||||
const Wrapped: React.FC<P> = (props) => {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={fallback}>
|
|
||||||
<Component {...props} />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Wrapped.displayName = `withFallback(${Component.displayName! || Component.name || 'Component'
|
|
||||||
})`;
|
|
||||||
|
|
||||||
return Wrapped;
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { NavData } from '@/lib/navData';
|
import type { NavData } from '@/lib/navData';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import NixOSLogo from '@/assets/nixos.svg?react';
|
import NixOSLogo from '@/assets/nixos.svg?react';
|
||||||
import { NavMain } from '@/components/sidebar/nav-main';
|
import { NavMain } from '@/components/sidebar/nav-main.view';
|
||||||
import { NavSecondary } from '@/components/sidebar/nav-secondary';
|
import { NavSecondary } from '@/components/sidebar/nav-secondary.view';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -12,9 +12,8 @@ import {
|
|||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import { NavUser } from './nav-user';
|
|
||||||
|
|
||||||
export function AppSidebar({ navData, ...props }: React.ComponentProps<typeof Sidebar> & { navData: NavData }) {
|
export function AppSidebar({ navData, footerWidget, ...props }: React.ComponentProps<typeof Sidebar> & { navData: NavData; footerWidget: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="offcanvas" {...props}>
|
<Sidebar collapsible="offcanvas" {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
@@ -37,7 +36,7 @@ export function AppSidebar({ navData, ...props }: React.ComponentProps<typeof Si
|
|||||||
<NavSecondary items={navData.navSecondary} className="mt-auto" />
|
<NavSecondary items={navData.navSecondary} className="mt-auto" />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavUser />
|
{footerWidget}
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
11
client/cms/src/components/sidebar/nav-user.container.tsx
Normal file
11
client/cms/src/components/sidebar/nav-user.container.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
||||||
|
import { NavUserView } from './nav-user.view';
|
||||||
|
|
||||||
|
export function NavUserContainer() {
|
||||||
|
const { data } = useUserInfo();
|
||||||
|
return (
|
||||||
|
<NavUserView
|
||||||
|
user={data.data!}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
client/cms/src/components/sidebar/nav-user.skeletion.tsx
Normal file
18
client/cms/src/components/sidebar/nav-user.skeletion.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { IconDotsVertical } from '@tabler/icons-react';
|
||||||
|
import { SidebarMenuButton } from '../ui/sidebar';
|
||||||
|
import { Skeleton } from '../ui/skeleton';
|
||||||
|
|
||||||
|
export function NavUserSkeleton() {
|
||||||
|
return (
|
||||||
|
<SidebarMenuButton
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||||
|
<div className="flex flex-col flex-1 gap-1">
|
||||||
|
<Skeleton className="h-3 w-16" />
|
||||||
|
<Skeleton className="h-3 w-24" />
|
||||||
|
</div>
|
||||||
|
<IconDotsVertical className="ml-auto size-4" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { identicon } from '@dicebear/collection';
|
import type { ServiceUserUserInfoData } from '@/client';
|
||||||
|
|
||||||
|
import { identicon } from '@dicebear/collection';
|
||||||
import { createAvatar } from '@dicebear/core';
|
import { createAvatar } from '@dicebear/core';
|
||||||
import {
|
import {
|
||||||
IconDotsVertical,
|
IconDotsVertical,
|
||||||
@@ -25,15 +26,10 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
|
||||||
import { logout } from '@/lib/token';
|
import { logout } from '@/lib/token';
|
||||||
import { withFallback } from '../hoc/with-fallback';
|
|
||||||
import { Skeleton } from '../ui/skeleton';
|
|
||||||
|
|
||||||
export function NavUser_() {
|
export function NavUserView({ user }: { user: ServiceUserUserInfoData }) {
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
const { data } = useUserInfo();
|
|
||||||
const user = data.data!;
|
|
||||||
|
|
||||||
const IdentIcon = useMemo(() => {
|
const IdentIcon = useMemo(() => {
|
||||||
const avatar = createAvatar(identicon, {
|
const avatar = createAvatar(identicon, {
|
||||||
@@ -94,20 +90,3 @@ export function NavUser_() {
|
|||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavUserSkeleton() {
|
|
||||||
return (
|
|
||||||
<SidebarMenuButton
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
|
||||||
<div className="flex flex-col flex-1 gap-1">
|
|
||||||
<Skeleton className="h-3 w-16" />
|
|
||||||
<Skeleton className="h-3 w-24" />
|
|
||||||
</div>
|
|
||||||
<IconDotsVertical className="ml-auto size-4" />
|
|
||||||
</SidebarMenuButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NavUser = withFallback(NavUser_, <NavUserSkeleton />);
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { createFileRoute, Outlet, useRouterState } from '@tanstack/react-router';
|
import { createFileRoute, Outlet, useRouterState } from '@tanstack/react-router';
|
||||||
import { AppSidebar } from '@/components/sidebar/app-sidebar';
|
import { Suspense } from 'react';
|
||||||
|
import { AppSidebar } from '@/components/sidebar/app-sidebar.view';
|
||||||
|
import { NavUserContainer } from '@/components/sidebar/nav-user.container';
|
||||||
|
import { NavUserSkeleton } from '@/components/sidebar/nav-user.skeletion';
|
||||||
import { SiteHeader } from '@/components/site-header';
|
import { SiteHeader } from '@/components/site-header';
|
||||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
||||||
import { navData } from '@/lib/navData';
|
import { navData } from '@/lib/navData';
|
||||||
@@ -27,7 +30,15 @@ function RouteComponent() {
|
|||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AppSidebar navData={navData} variant="inset" />
|
<AppSidebar
|
||||||
|
navData={navData}
|
||||||
|
footerWidget={(
|
||||||
|
<Suspense fallback={<NavUserSkeleton />}>
|
||||||
|
<NavUserContainer />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
variant="inset"
|
||||||
|
/>
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<SiteHeader title={title} />
|
<SiteHeader title={title} />
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col">
|
||||||
|
|||||||
8
client/cms/src/stories/exampleUser.ts
Normal file
8
client/cms/src/stories/exampleUser.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const user = {
|
||||||
|
username: 'nvirellia',
|
||||||
|
nickname: 'Noa Virellia',
|
||||||
|
subtitle: '天生骄傲',
|
||||||
|
email: 'noa@requiem.garden',
|
||||||
|
bio: '',
|
||||||
|
avatar: 'https://avatars.githubusercontent.com/u/54884471?v=4',
|
||||||
|
};
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
||||||
import { NavUser_ } from '@/components/sidebar/nav-user';
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
title: 'NavUser',
|
|
||||||
component: NavUser_,
|
|
||||||
} satisfies Meta<typeof NavUser_>;
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof meta>;
|
|
||||||
|
|
||||||
export const Primary: Story = {};
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
import { EditProfileDialogView } from '@/components/profile/edit-profile.dialog.view';
|
import { EditProfileDialogView } from '@/components/profile/edit-profile.dialog.view';
|
||||||
|
import { user } from '../exampleUser';
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Profile/EditDialog',
|
title: 'Profile/EditDialog',
|
||||||
@@ -27,14 +28,7 @@ type Story = StoryObj<typeof meta>;
|
|||||||
|
|
||||||
export const Primary: Story = {
|
export const Primary: Story = {
|
||||||
args: {
|
args: {
|
||||||
user: {
|
user,
|
||||||
username: 'nvirellia',
|
|
||||||
nickname: 'Noa Virellia',
|
|
||||||
subtitle: '天生骄傲',
|
|
||||||
email: 'noa@requiem.garden',
|
|
||||||
bio: '',
|
|
||||||
avatar: 'https://avatars.githubusercontent.com/u/54884471?v=4',
|
|
||||||
},
|
|
||||||
updateProfile: async () => { },
|
updateProfile: async () => { },
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|||||||
import { ProfileError } from '@/components/profile/profile.error';
|
import { ProfileError } from '@/components/profile/profile.error';
|
||||||
import { ProfileSkeleton } from '@/components/profile/profile.skeleton';
|
import { ProfileSkeleton } from '@/components/profile/profile.skeleton';
|
||||||
import { ProfileView } from '@/components/profile/profile.view';
|
import { ProfileView } from '@/components/profile/profile.view';
|
||||||
|
import { user } from '../exampleUser';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -23,20 +24,13 @@ type Story = StoryObj<typeof meta>;
|
|||||||
|
|
||||||
export const Primary: Story = {
|
export const Primary: Story = {
|
||||||
args: {
|
args: {
|
||||||
user: {
|
user,
|
||||||
username: 'nvirellia',
|
|
||||||
nickname: 'Noa Virellia',
|
|
||||||
subtitle: '天生骄傲',
|
|
||||||
email: 'noa@requiem.garden',
|
|
||||||
bio: '',
|
|
||||||
avatar: 'https://avatars.githubusercontent.com/u/54884471?v=4',
|
|
||||||
},
|
|
||||||
onSaveBio: async () => Promise.resolve(),
|
onSaveBio: async () => Promise.resolve(),
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Skeleton: Story = {
|
export const Loading: Story = {
|
||||||
render: () => <ProfileSkeleton />,
|
render: () => <ProfileSkeleton />,
|
||||||
args: {
|
args: {
|
||||||
user: {},
|
user: {},
|
||||||
|
|||||||
43
client/cms/src/stories/sidebar.stories.tsx
Normal file
43
client/cms/src/stories/sidebar.stories.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { AppSidebar } from '@/components/sidebar/app-sidebar.view';
|
||||||
|
import { NavUserSkeleton } from '@/components/sidebar/nav-user.skeletion';
|
||||||
|
import { NavUserView } from '@/components/sidebar/nav-user.view';
|
||||||
|
import { SidebarProvider } from '@/components/ui/sidebar';
|
||||||
|
import { navData } from '@/lib/navData';
|
||||||
|
import { user } from './exampleUser';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Navigation/Sidebar',
|
||||||
|
component: AppSidebar,
|
||||||
|
decorators: [
|
||||||
|
Story => (
|
||||||
|
<SidebarProvider
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
'--sidebar-width': 'calc(var(--spacing) * 72)',
|
||||||
|
'--header-height': 'calc(var(--spacing) * 12)',
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Story />
|
||||||
|
</SidebarProvider>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
} satisfies Meta<typeof AppSidebar>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Primary: Story = {
|
||||||
|
args: {
|
||||||
|
navData,
|
||||||
|
footerWidget: <NavUserView user={user} />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Loading: Story = {
|
||||||
|
args: {
|
||||||
|
navData,
|
||||||
|
footerWidget: <NavUserSkeleton />,
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user