refactor(profile): split view/container and update nav state
Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
18
client/cms/.zed/settings.json
Normal file
18
client/cms/.zed/settings.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"file_scan_exclusions": [
|
||||||
|
"src/components/ui",
|
||||||
|
".tanstack",
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
|
||||||
|
// default values below
|
||||||
|
"**/.git",
|
||||||
|
"**/.svn",
|
||||||
|
"**/.hg",
|
||||||
|
"**/CVS",
|
||||||
|
"**/.DS_Store",
|
||||||
|
"**/Thumbs.db",
|
||||||
|
"**/.classpath",
|
||||||
|
"**/.settings",
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { ServiceUserUserInfoData } from '@/client';
|
||||||
|
import { useUpdateUser } from '@/hooks/data/useUpdateUser';
|
||||||
|
import { EditProfileDialogView } from './edit-profile.dialog.view';
|
||||||
|
|
||||||
|
export function EditProfileDialogContainer({ data }: { data: ServiceUserUserInfoData }) {
|
||||||
|
const { mutateAsync } = useUpdateUser();
|
||||||
|
return (
|
||||||
|
<EditProfileDialogView
|
||||||
|
user={data}
|
||||||
|
updateProfile={async (data) => {
|
||||||
|
await mutateAsync({ body: data });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { ServiceUserUserInfoData } from '@/client';
|
import type { ServiceUserUserInfoData } from '@/client';
|
||||||
import { useForm } from '@tanstack/react-form';
|
import { useForm } from '@tanstack/react-form';
|
||||||
import { useState } from 'react';
|
import {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -21,7 +24,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
} from '@/components/ui/input';
|
} from '@/components/ui/input';
|
||||||
import { useUpdateUser } from '@/hooks/data/useUpdateUser';
|
|
||||||
import { Switch } from '../ui/switch';
|
import { Switch } from '../ui/switch';
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -31,9 +33,7 @@ const formSchema = z.object({
|
|||||||
avatar: z.url().or(z.literal('')),
|
avatar: z.url().or(z.literal('')),
|
||||||
allow_public: z.boolean(),
|
allow_public: z.boolean(),
|
||||||
});
|
});
|
||||||
export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) {
|
export function EditProfileDialogView({ user, updateProfile }: { user: ServiceUserUserInfoData; updateProfile: (data: ServiceUserUserInfoData) => Promise<void> }) {
|
||||||
const { mutateAsync } = useUpdateUser();
|
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
avatar: user.avatar,
|
avatar: user.avatar,
|
||||||
@@ -49,7 +49,7 @@ export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) {
|
|||||||
value,
|
value,
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
await mutateAsync({ body: value });
|
await updateProfile(value);
|
||||||
toast.success('个人资料更新成功');
|
toast.success('个人资料更新成功');
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -61,11 +61,14 @@ export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) {
|
|||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setTimeout(() => {
|
const id = setTimeout(() => {
|
||||||
form.reset();
|
form.reset();
|
||||||
}, 200);
|
}, 200);
|
||||||
|
return () => clearTimeout(id);
|
||||||
}
|
}
|
||||||
|
}, [open, form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
17
client/cms/src/components/profile/profile.container.tsx
Normal file
17
client/cms/src/components/profile/profile.container.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useUpdateUser } from '@/hooks/data/useUpdateUser';
|
||||||
|
import { useOtherUserInfo } from '@/hooks/data/useUserInfo';
|
||||||
|
import { utf8ToBase64 } from '@/lib/utils';
|
||||||
|
import { ProfileView } from './profile.view';
|
||||||
|
|
||||||
|
export function ProfileContainer({ userId }: { userId: string }) {
|
||||||
|
const { data } = useOtherUserInfo(userId);
|
||||||
|
const { mutateAsync } = useUpdateUser();
|
||||||
|
return (
|
||||||
|
<ProfileView
|
||||||
|
user={data.data!}
|
||||||
|
onSaveBio={async (bio) => {
|
||||||
|
await mutateAsync({ body: { bio: utf8ToBase64(bio) } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,15 +11,13 @@ import { useMemo, useState } from 'react';
|
|||||||
import Markdown from 'react-markdown';
|
import Markdown from 'react-markdown';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Avatar, AvatarImage } from '@/components/ui/avatar';
|
import { Avatar, AvatarImage } from '@/components/ui/avatar';
|
||||||
import { useUpdateUser } from '@/hooks/data/useUpdateUser';
|
import { base64ToUtf8 } from '@/lib/utils';
|
||||||
import { base64ToUtf8, utf8ToBase64 } from '@/lib/utils';
|
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { EditProfileDialog } from './edit-profile-dialog';
|
import { EditProfileDialogContainer } from './edit-profile.dialog.container';
|
||||||
|
|
||||||
export function Profile({ user }: { user: ServiceUserUserInfoData }) {
|
export function ProfileView({ user, onSaveBio }: { user: ServiceUserUserInfoData; onSaveBio: (bio: string) => Promise<void> }) {
|
||||||
const [bio, setBio] = useState<string | undefined>(() => base64ToUtf8(user.bio ?? ''));
|
const [bio, setBio] = useState<string | undefined>(() => base64ToUtf8(user.bio ?? ''));
|
||||||
const [enableBioEdit, setEnableBioEdit] = useState(false);
|
const [enableBioEdit, setEnableBioEdit] = useState(false);
|
||||||
const { mutateAsync } = useUpdateUser();
|
|
||||||
|
|
||||||
const IdentIcon = useMemo(() => {
|
const IdentIcon = useMemo(() => {
|
||||||
const avatar = createAvatar(identicon, {
|
const avatar = createAvatar(identicon, {
|
||||||
@@ -48,7 +46,7 @@ export function Profile({ user }: { user: ServiceUserUserInfoData }) {
|
|||||||
{user.email}
|
{user.email}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EditProfileDialog user={user} />
|
<EditProfileDialogContainer data={user} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section className="relative rounded-md border border-muted w-full flex-1 lg:flex-auto min-h-72 lg:h-full mt-4 lg:mt-0 prose dark:prose-invert max-w-[1012px] self-center">
|
<section className="relative rounded-md border border-muted w-full flex-1 lg:flex-auto min-h-72 lg:h-full mt-4 lg:mt-0 prose dark:prose-invert max-w-[1012px] self-center">
|
||||||
@@ -72,7 +70,7 @@ export function Profile({ user }: { user: ServiceUserUserInfoData }) {
|
|||||||
else {
|
else {
|
||||||
if (!isNil(bio)) {
|
if (!isNil(bio)) {
|
||||||
try {
|
try {
|
||||||
await mutateAsync({ body: { bio: utf8ToBase64(bio) } });
|
await onSaveBio(bio);
|
||||||
setEnableBioEdit(false);
|
setEnableBioEdit(false);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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';
|
||||||
@@ -11,10 +12,9 @@ import {
|
|||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import { navData } from '@/lib/navData';
|
|
||||||
import { NavUser } from './nav-user';
|
import { NavUser } from './nav-user';
|
||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ navData, ...props }: React.ComponentProps<typeof Sidebar> & { navData: NavData }) {
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="offcanvas" {...props}>
|
<Sidebar collapsible="offcanvas" {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
|
|||||||
@@ -26,15 +26,14 @@ import {
|
|||||||
useSidebar,
|
useSidebar,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
||||||
import { useLogout } from '@/hooks/useLogout';
|
import { logout } from '@/lib/token';
|
||||||
import { withFallback } from '../hoc/with-fallback';
|
import { withFallback } from '../hoc/with-fallback';
|
||||||
import { Skeleton } from '../ui/skeleton';
|
import { Skeleton } from '../ui/skeleton';
|
||||||
|
|
||||||
function NavUser_() {
|
export function NavUser_() {
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
const { data } = useUserInfo();
|
const { data } = useUserInfo();
|
||||||
const user = data.data!;
|
const user = data.data!;
|
||||||
const { logout } = useLogout();
|
|
||||||
|
|
||||||
const IdentIcon = useMemo(() => {
|
const IdentIcon = useMemo(() => {
|
||||||
const avatar = createAvatar(identicon, {
|
const avatar = createAvatar(identicon, {
|
||||||
@@ -85,7 +84,7 @@ function NavUser_() {
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={logout}>
|
<DropdownMenuItem onClick={_e => logout()}>
|
||||||
<IconLogout />
|
<IconLogout />
|
||||||
登出
|
登出
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -1,18 +1,7 @@
|
|||||||
import { useRouterState } from '@tanstack/react-router';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { SidebarTrigger } from '@/components/ui/sidebar';
|
import { SidebarTrigger } from '@/components/ui/sidebar';
|
||||||
import { navData } from '@/lib/navData';
|
|
||||||
|
|
||||||
export function SiteHeader() {
|
|
||||||
const pathname = useRouterState({ select: state => state.location.pathname });
|
|
||||||
const allNavItems = [...navData.navMain, ...navData.navSecondary];
|
|
||||||
const currentTitle
|
|
||||||
= allNavItems.find(item =>
|
|
||||||
item.url === '/'
|
|
||||||
? pathname === '/'
|
|
||||||
: pathname.startsWith(item.url),
|
|
||||||
)?.title ?? '工作台';
|
|
||||||
|
|
||||||
|
export function SiteHeader({ title }: { title: string }) {
|
||||||
return (
|
return (
|
||||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||||
@@ -21,7 +10,7 @@ export function SiteHeader() {
|
|||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
className="mx-2 data-[orientation=vertical]:h-4"
|
className="mx-2 data-[orientation=vertical]:h-4"
|
||||||
/>
|
/>
|
||||||
<h1 className="text-base font-medium">{currentTitle}</h1>
|
<h1 className="text-base font-medium">{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
|
import type { ServiceUserUserInfoData } from '@/client';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { getUserInfoQueryKey, patchUserUpdateMutation } from '@/client/@tanstack/react-query.gen';
|
import { getUserInfoByUserIdQueryKey, getUserInfoQueryKey, patchUserUpdateMutation } from '@/client/@tanstack/react-query.gen';
|
||||||
|
|
||||||
export function useUpdateUser() {
|
export function useUpdateUser() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const data: { data: ServiceUserUserInfoData | undefined } | undefined = queryClient.getQueryData(getUserInfoQueryKey());
|
||||||
return useMutation({
|
return useMutation({
|
||||||
...patchUserUpdateMutation(),
|
...patchUserUpdateMutation(),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await queryClient.invalidateQueries({ queryKey: getUserInfoQueryKey() });
|
await queryClient.invalidateQueries({ queryKey: getUserInfoQueryKey() });
|
||||||
|
if ((data?.data?.user_id) != null) {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: getUserInfoByUserIdQueryKey({ path: { user_id: data.data.user_id } }) });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { useNavigate } from '@tanstack/react-router';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { clearTokens } from '@/lib/token';
|
|
||||||
|
|
||||||
export function useLogout() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
|
||||||
clearTokens();
|
|
||||||
void navigate({ to: '/authorize' });
|
|
||||||
}, [navigate]);
|
|
||||||
|
|
||||||
return { logout };
|
|
||||||
}
|
|
||||||
@@ -25,3 +25,4 @@ export const navData = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
export type NavData = typeof navData;
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ export async function doRefreshToken(refreshToken: string): Promise<ServiceAuthT
|
|||||||
return data?.data;
|
return data?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logout(message: string = 'Logged out') {
|
export function logout(message: string = '已登出') {
|
||||||
clearTokens();
|
clearTokens();
|
||||||
void router.navigate({ to: '/authorize' }).then(() => {
|
void router.navigate({ to: '/authorize' }).then(() => {
|
||||||
toast.error(message);
|
toast.info(message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,9 @@ const queryClient = new QueryClient({
|
|||||||
const status
|
const status
|
||||||
// eslint-disable-next-line ts/no-unsafe-member-access
|
// eslint-disable-next-line ts/no-unsafe-member-access
|
||||||
= error?.response?.status ?? error?.status;
|
= error?.response?.status ?? error?.status;
|
||||||
|
|
||||||
if (status >= 400 && status < 500) {
|
if (status >= 400 && status < 500) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return failureCount < 3;
|
return failureCount < 3;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
import { createFileRoute, Outlet, useRouterState } from '@tanstack/react-router';
|
||||||
import { AppSidebar } from '@/components/sidebar/app-sidebar';
|
import { AppSidebar } from '@/components/sidebar/app-sidebar';
|
||||||
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';
|
||||||
|
|
||||||
export const Route = createFileRoute('/_workbenchLayout')({
|
export const Route = createFileRoute('/_workbenchLayout')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
|
const pathname = useRouterState({ select: state => state.location.pathname });
|
||||||
|
const allNavItems = [...navData.navMain, ...navData.navSecondary];
|
||||||
|
const title
|
||||||
|
= allNavItems.find(item =>
|
||||||
|
item.url === '/'
|
||||||
|
? pathname === '/'
|
||||||
|
: pathname.startsWith(item.url),
|
||||||
|
)?.title ?? '工作台';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<SidebarProvider
|
||||||
style={
|
style={
|
||||||
@@ -17,9 +27,9 @@ function RouteComponent() {
|
|||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AppSidebar variant="inset" />
|
<AppSidebar navData={navData} variant="inset" />
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<SiteHeader />
|
<SiteHeader title={title} />
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col">
|
||||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { Profile } from '@/components/profile/profile';
|
import { ProfileContainer } from '@/components/profile/profile.container';
|
||||||
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 { useOtherUserInfo } from '@/hooks/data/useUserInfo';
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/_workbenchLayout/profile/$userId')({
|
export const Route = createFileRoute('/_workbenchLayout/profile/$userId')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function ProfileByUserId({ userId }: { userId: string }) {
|
|
||||||
const { data } = useOtherUserInfo(userId);
|
|
||||||
return <Profile user={data.data!} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { userId } = Route.useParams();
|
const { userId } = Route.useParams();
|
||||||
return (
|
return (
|
||||||
@@ -26,7 +20,7 @@ function RouteComponent() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Suspense fallback={<ProfileSkeleton />}>
|
<Suspense fallback={<ProfileSkeleton />}>
|
||||||
<ProfileByUserId userId={userId} />
|
<ProfileContainer userId={userId} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { createFileRoute, Navigate } from '@tanstack/react-router';
|
import { createFileRoute, Navigate } from '@tanstack/react-router';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import { ProfileError } from '@/components/profile/profile.error';
|
||||||
|
import { ProfileSkeleton } from '@/components/profile/profile.skeleton';
|
||||||
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
||||||
|
|
||||||
export const Route = createFileRoute('/_workbenchLayout/profile/')({
|
export const Route = createFileRoute('/_workbenchLayout/profile/')({
|
||||||
@@ -8,6 +12,10 @@ export const Route = createFileRoute('/_workbenchLayout/profile/')({
|
|||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { data } = useUserInfo();
|
const { data } = useUserInfo();
|
||||||
return (
|
return (
|
||||||
|
<ErrorBoundary fallback={<ProfileError reason="获取用户个人资料失败" />}>
|
||||||
|
<Suspense fallback={<ProfileSkeleton />}>
|
||||||
<Navigate to="/profile/$userId" params={{ userId: data.data!.user_id! }} />
|
<Navigate to="/profile/$userId" params={{ userId: data.data!.user_id! }} />
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function RouteComponent() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [token, mutation.isIdle]);
|
}, [token, mutation.isIdle, mutation, oauthParams.client_id, oauthParams.redirect_uri, oauthParams.state]);
|
||||||
return (
|
return (
|
||||||
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm">
|
||||||
|
|||||||
12
client/cms/src/stories/nav-user.stories.tsx
Normal file
12
client/cms/src/stories/nav-user.stories.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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 = {};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { EditProfileDialogView } from '@/components/profile/edit-profile.dialog.view';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Profile/EditDialog',
|
||||||
|
component: EditProfileDialogView,
|
||||||
|
decorators: [
|
||||||
|
Story => (
|
||||||
|
<div style={{
|
||||||
|
height: '100vh',
|
||||||
|
maxWidth: '256px',
|
||||||
|
margin: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
} satisfies Meta<typeof EditProfileDialogView>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
updateProfile: async () => { },
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { Profile } from '@/components/profile/profile';
|
|
||||||
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';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Profile',
|
title: 'Profile/View',
|
||||||
component: Profile,
|
component: ProfileView,
|
||||||
decorators: [
|
decorators: [
|
||||||
Story => (
|
Story => (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
@@ -16,7 +16,7 @@ const meta = {
|
|||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
} satisfies Meta<typeof Profile>;
|
} satisfies Meta<typeof ProfileView>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
@@ -31,13 +31,16 @@ export const Primary: Story = {
|
|||||||
bio: '',
|
bio: '',
|
||||||
avatar: 'https://avatars.githubusercontent.com/u/54884471?v=4',
|
avatar: 'https://avatars.githubusercontent.com/u/54884471?v=4',
|
||||||
},
|
},
|
||||||
|
onSaveBio: async () => Promise.resolve(),
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Skeleton: Story = {
|
export const Skeleton: Story = {
|
||||||
render: () => <ProfileSkeleton />,
|
render: () => <ProfileSkeleton />,
|
||||||
args: {
|
args: {
|
||||||
user: {},
|
user: {},
|
||||||
|
onSaveBio: async () => Promise.resolve(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,5 +50,6 @@ export const Error: Story = {
|
|||||||
user: {
|
user: {
|
||||||
allow_public: false,
|
allow_public: false,
|
||||||
},
|
},
|
||||||
|
onSaveBio: async () => Promise.resolve(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user