From 65d493b91bff39df2efe41f9f977f65c390f2d58 Mon Sep 17 00:00:00 2001 From: Noa Virellia Date: Sat, 31 Jan 2026 18:30:34 +0800 Subject: [PATCH] refactor(profile): split view/container and update nav state Signed-off-by: Noa Virellia --- client/cms/.zed/settings.json | 18 ++++++++ .../profile/edit-profile.dialog.container.tsx | 15 +++++++ ...ialog.tsx => edit-profile.dialog.view.tsx} | 25 ++++++----- .../components/profile/profile.container.tsx | 17 ++++++++ .../profile/{profile.tsx => profile.view.tsx} | 12 +++--- .../src/components/sidebar/app-sidebar.tsx | 4 +- .../cms/src/components/sidebar/nav-user.tsx | 7 ++- client/cms/src/components/site-header.tsx | 15 +------ client/cms/src/hooks/data/useUpdateUser.ts | 7 ++- client/cms/src/hooks/useLogout.ts | 14 ------ client/cms/src/lib/navData.ts | 1 + client/cms/src/lib/token.ts | 4 +- client/cms/src/routes/__root.tsx | 2 - client/cms/src/routes/_workbenchLayout.tsx | 16 +++++-- .../_workbenchLayout/profile.$userId.tsx | 10 +---- .../routes/_workbenchLayout/profile.index.tsx | 10 ++++- client/cms/src/routes/authorize.tsx | 2 +- client/cms/src/stories/nav-user.stories.tsx | 12 ++++++ .../profile/edit-profile.dialog.stories.tsx | 43 +++++++++++++++++++ .../stories/{ => profile}/profile.stories.tsx | 12 ++++-- 20 files changed, 173 insertions(+), 73 deletions(-) create mode 100644 client/cms/.zed/settings.json create mode 100644 client/cms/src/components/profile/edit-profile.dialog.container.tsx rename client/cms/src/components/profile/{edit-profile-dialog.tsx => edit-profile.dialog.view.tsx} (92%) create mode 100644 client/cms/src/components/profile/profile.container.tsx rename client/cms/src/components/profile/{profile.tsx => profile.view.tsx} (88%) delete mode 100644 client/cms/src/hooks/useLogout.ts create mode 100644 client/cms/src/stories/nav-user.stories.tsx create mode 100644 client/cms/src/stories/profile/edit-profile.dialog.stories.tsx rename client/cms/src/stories/{ => profile}/profile.stories.tsx (78%) diff --git a/client/cms/.zed/settings.json b/client/cms/.zed/settings.json new file mode 100644 index 0000000..aa63caf --- /dev/null +++ b/client/cms/.zed/settings.json @@ -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", + ], +} diff --git a/client/cms/src/components/profile/edit-profile.dialog.container.tsx b/client/cms/src/components/profile/edit-profile.dialog.container.tsx new file mode 100644 index 0000000..53cd883 --- /dev/null +++ b/client/cms/src/components/profile/edit-profile.dialog.container.tsx @@ -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 ( + { + await mutateAsync({ body: data }); + }} + /> + ); +} diff --git a/client/cms/src/components/profile/edit-profile-dialog.tsx b/client/cms/src/components/profile/edit-profile.dialog.view.tsx similarity index 92% rename from client/cms/src/components/profile/edit-profile-dialog.tsx rename to client/cms/src/components/profile/edit-profile.dialog.view.tsx index 923f312..e4437c4 100644 --- a/client/cms/src/components/profile/edit-profile-dialog.tsx +++ b/client/cms/src/components/profile/edit-profile.dialog.view.tsx @@ -1,6 +1,9 @@ import type { ServiceUserUserInfoData } from '@/client'; import { useForm } from '@tanstack/react-form'; -import { useState } from 'react'; +import { + useEffect, + useState, +} from 'react'; import { toast } from 'sonner'; import z from 'zod'; import { Button } from '@/components/ui/button'; @@ -21,7 +24,6 @@ import { import { Input, } from '@/components/ui/input'; -import { useUpdateUser } from '@/hooks/data/useUpdateUser'; import { Switch } from '../ui/switch'; const formSchema = z.object({ @@ -31,9 +33,7 @@ const formSchema = z.object({ avatar: z.url().or(z.literal('')), allow_public: z.boolean(), }); -export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) { - const { mutateAsync } = useUpdateUser(); - +export function EditProfileDialogView({ user, updateProfile }: { user: ServiceUserUserInfoData; updateProfile: (data: ServiceUserUserInfoData) => Promise }) { const form = useForm({ defaultValues: { avatar: user.avatar, @@ -49,7 +49,7 @@ export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) { value, }) => { try { - await mutateAsync({ body: value }); + await updateProfile(value); toast.success('个人资料更新成功'); } catch (error) { @@ -61,11 +61,14 @@ export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) { const [open, setOpen] = useState(false); - if (!open) { - setTimeout(() => { - form.reset(); - }, 200); - } + useEffect(() => { + if (!open) { + const id = setTimeout(() => { + form.reset(); + }, 200); + return () => clearTimeout(id); + } + }, [open, form]); return ( diff --git a/client/cms/src/components/profile/profile.container.tsx b/client/cms/src/components/profile/profile.container.tsx new file mode 100644 index 0000000..c82593e --- /dev/null +++ b/client/cms/src/components/profile/profile.container.tsx @@ -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 ( + { + await mutateAsync({ body: { bio: utf8ToBase64(bio) } }); + }} + /> + ); +} diff --git a/client/cms/src/components/profile/profile.tsx b/client/cms/src/components/profile/profile.view.tsx similarity index 88% rename from client/cms/src/components/profile/profile.tsx rename to client/cms/src/components/profile/profile.view.tsx index 4edc8ef..0669fdf 100644 --- a/client/cms/src/components/profile/profile.tsx +++ b/client/cms/src/components/profile/profile.view.tsx @@ -11,15 +11,13 @@ import { useMemo, useState } from 'react'; import Markdown from 'react-markdown'; import { toast } from 'sonner'; import { Avatar, AvatarImage } from '@/components/ui/avatar'; -import { useUpdateUser } from '@/hooks/data/useUpdateUser'; -import { base64ToUtf8, utf8ToBase64 } from '@/lib/utils'; +import { base64ToUtf8 } from '@/lib/utils'; 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 }) { const [bio, setBio] = useState(() => base64ToUtf8(user.bio ?? '')); const [enableBioEdit, setEnableBioEdit] = useState(false); - const { mutateAsync } = useUpdateUser(); const IdentIcon = useMemo(() => { const avatar = createAvatar(identicon, { @@ -48,7 +46,7 @@ export function Profile({ user }: { user: ServiceUserUserInfoData }) { {user.email} - +
@@ -72,7 +70,7 @@ export function Profile({ user }: { user: ServiceUserUserInfoData }) { else { if (!isNil(bio)) { try { - await mutateAsync({ body: { bio: utf8ToBase64(bio) } }); + await onSaveBio(bio); setEnableBioEdit(false); } catch (error) { diff --git a/client/cms/src/components/sidebar/app-sidebar.tsx b/client/cms/src/components/sidebar/app-sidebar.tsx index 0d1a945..5b971d5 100644 --- a/client/cms/src/components/sidebar/app-sidebar.tsx +++ b/client/cms/src/components/sidebar/app-sidebar.tsx @@ -1,3 +1,4 @@ +import type { NavData } from '@/lib/navData'; import * as React from 'react'; import NixOSLogo from '@/assets/nixos.svg?react'; import { NavMain } from '@/components/sidebar/nav-main'; @@ -11,10 +12,9 @@ import { SidebarMenuButton, SidebarMenuItem, } from '@/components/ui/sidebar'; -import { navData } from '@/lib/navData'; import { NavUser } from './nav-user'; -export function AppSidebar({ ...props }: React.ComponentProps) { +export function AppSidebar({ navData, ...props }: React.ComponentProps & { navData: NavData }) { return ( diff --git a/client/cms/src/components/sidebar/nav-user.tsx b/client/cms/src/components/sidebar/nav-user.tsx index 2384aec..b60a467 100644 --- a/client/cms/src/components/sidebar/nav-user.tsx +++ b/client/cms/src/components/sidebar/nav-user.tsx @@ -26,15 +26,14 @@ import { useSidebar, } from '@/components/ui/sidebar'; import { useUserInfo } from '@/hooks/data/useUserInfo'; -import { useLogout } from '@/hooks/useLogout'; +import { logout } from '@/lib/token'; import { withFallback } from '../hoc/with-fallback'; import { Skeleton } from '../ui/skeleton'; -function NavUser_() { +export function NavUser_() { const { isMobile } = useSidebar(); const { data } = useUserInfo(); const user = data.data!; - const { logout } = useLogout(); const IdentIcon = useMemo(() => { const avatar = createAvatar(identicon, { @@ -85,7 +84,7 @@ function NavUser_() { - + logout()}> 登出 diff --git a/client/cms/src/components/site-header.tsx b/client/cms/src/components/site-header.tsx index 9ecc44b..4eb3e5c 100644 --- a/client/cms/src/components/site-header.tsx +++ b/client/cms/src/components/site-header.tsx @@ -1,18 +1,7 @@ -import { useRouterState } from '@tanstack/react-router'; import { Separator } from '@/components/ui/separator'; 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 (
@@ -21,7 +10,7 @@ export function SiteHeader() { orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" /> -

{currentTitle}

+

{title}

); diff --git a/client/cms/src/hooks/data/useUpdateUser.ts b/client/cms/src/hooks/data/useUpdateUser.ts index 9fe2efe..d99a302 100644 --- a/client/cms/src/hooks/data/useUpdateUser.ts +++ b/client/cms/src/hooks/data/useUpdateUser.ts @@ -1,12 +1,17 @@ +import type { ServiceUserUserInfoData } from '@/client'; 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() { const queryClient = useQueryClient(); + const data: { data: ServiceUserUserInfoData | undefined } | undefined = queryClient.getQueryData(getUserInfoQueryKey()); return useMutation({ ...patchUserUpdateMutation(), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: getUserInfoQueryKey() }); + if ((data?.data?.user_id) != null) { + await queryClient.invalidateQueries({ queryKey: getUserInfoByUserIdQueryKey({ path: { user_id: data.data.user_id } }) }); + } }, }); } diff --git a/client/cms/src/hooks/useLogout.ts b/client/cms/src/hooks/useLogout.ts deleted file mode 100644 index e8f731c..0000000 --- a/client/cms/src/hooks/useLogout.ts +++ /dev/null @@ -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 }; -} diff --git a/client/cms/src/lib/navData.ts b/client/cms/src/lib/navData.ts index 83be1ad..f5f24ad 100644 --- a/client/cms/src/lib/navData.ts +++ b/client/cms/src/lib/navData.ts @@ -25,3 +25,4 @@ export const navData = { }, ], }; +export type NavData = typeof navData; diff --git a/client/cms/src/lib/token.ts b/client/cms/src/lib/token.ts index 96c745c..d491945 100644 --- a/client/cms/src/lib/token.ts +++ b/client/cms/src/lib/token.ts @@ -44,9 +44,9 @@ export async function doRefreshToken(refreshToken: string): Promise { - toast.error(message); + toast.info(message); }); } diff --git a/client/cms/src/routes/__root.tsx b/client/cms/src/routes/__root.tsx index faf535a..1f0df9e 100644 --- a/client/cms/src/routes/__root.tsx +++ b/client/cms/src/routes/__root.tsx @@ -12,11 +12,9 @@ const queryClient = new QueryClient({ const status // eslint-disable-next-line ts/no-unsafe-member-access = error?.response?.status ?? error?.status; - if (status >= 400 && status < 500) { return false; } - return failureCount < 3; }, }, diff --git a/client/cms/src/routes/_workbenchLayout.tsx b/client/cms/src/routes/_workbenchLayout.tsx index a52b375..4bf19ef 100644 --- a/client/cms/src/routes/_workbenchLayout.tsx +++ b/client/cms/src/routes/_workbenchLayout.tsx @@ -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 { SiteHeader } from '@/components/site-header'; import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; +import { navData } from '@/lib/navData'; export const Route = createFileRoute('/_workbenchLayout')({ component: 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 ( - + - +
diff --git a/client/cms/src/routes/_workbenchLayout/profile.$userId.tsx b/client/cms/src/routes/_workbenchLayout/profile.$userId.tsx index 5f18e4a..f502805 100644 --- a/client/cms/src/routes/_workbenchLayout/profile.$userId.tsx +++ b/client/cms/src/routes/_workbenchLayout/profile.$userId.tsx @@ -1,20 +1,14 @@ import { createFileRoute } from '@tanstack/react-router'; import { Suspense } from 'react'; 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 { 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 ( @@ -26,7 +20,7 @@ function RouteComponent() { }} > }> - +
diff --git a/client/cms/src/routes/_workbenchLayout/profile.index.tsx b/client/cms/src/routes/_workbenchLayout/profile.index.tsx index 090e248..c4add66 100644 --- a/client/cms/src/routes/_workbenchLayout/profile.index.tsx +++ b/client/cms/src/routes/_workbenchLayout/profile.index.tsx @@ -1,4 +1,8 @@ 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'; export const Route = createFileRoute('/_workbenchLayout/profile/')({ @@ -8,6 +12,10 @@ export const Route = createFileRoute('/_workbenchLayout/profile/')({ function RouteComponent() { const { data } = useUserInfo(); return ( - + }> + }> + + + ); } diff --git a/client/cms/src/routes/authorize.tsx b/client/cms/src/routes/authorize.tsx index 05fd479..36404e5 100644 --- a/client/cms/src/routes/authorize.tsx +++ b/client/cms/src/routes/authorize.tsx @@ -41,7 +41,7 @@ function RouteComponent() { }, }); } - }, [token, mutation.isIdle]); + }, [token, mutation.isIdle, mutation, oauthParams.client_id, oauthParams.redirect_uri, oauthParams.state]); return (
diff --git a/client/cms/src/stories/nav-user.stories.tsx b/client/cms/src/stories/nav-user.stories.tsx new file mode 100644 index 0000000..679366a --- /dev/null +++ b/client/cms/src/stories/nav-user.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = {}; diff --git a/client/cms/src/stories/profile/edit-profile.dialog.stories.tsx b/client/cms/src/stories/profile/edit-profile.dialog.stories.tsx new file mode 100644 index 0000000..d3dcebf --- /dev/null +++ b/client/cms/src/stories/profile/edit-profile.dialog.stories.tsx @@ -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 => ( +
+ +
+ ), + ], +} 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', + }, + updateProfile: async () => { }, + }, + parameters: { + layout: 'fullscreen', + }, +}; diff --git a/client/cms/src/stories/profile.stories.tsx b/client/cms/src/stories/profile/profile.stories.tsx similarity index 78% rename from client/cms/src/stories/profile.stories.tsx rename to client/cms/src/stories/profile/profile.stories.tsx index c4e6152..8f84d18 100644 --- a/client/cms/src/stories/profile.stories.tsx +++ b/client/cms/src/stories/profile/profile.stories.tsx @@ -1,14 +1,14 @@ 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'; +import { ProfileView } from '@/components/profile/profile.view'; const queryClient = new QueryClient(); const meta = { - title: 'Profile', - component: Profile, + title: 'Profile/View', + component: ProfileView, decorators: [ Story => ( @@ -16,7 +16,7 @@ const meta = { ), ], -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -31,13 +31,16 @@ export const Primary: Story = { bio: '', avatar: 'https://avatars.githubusercontent.com/u/54884471?v=4', }, + onSaveBio: async () => Promise.resolve(), }, + }; export const Skeleton: Story = { render: () => , args: { user: {}, + onSaveBio: async () => Promise.resolve(), }, }; @@ -47,5 +50,6 @@ export const Error: Story = { user: { allow_public: false, }, + onSaveBio: async () => Promise.resolve(), }, };