feat(client): add storybook and workbench profile flow

Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
2026-01-31 12:29:35 +08:00
committed by Asai Neko
parent 22fdcd2020
commit ff10fe10ce
31 changed files with 1946 additions and 134 deletions

View File

@@ -1,3 +1,4 @@
import type { ServiceUserUserInfoData } from '@/client';
import { useForm } from '@tanstack/react-form';
import { useState } from 'react';
import { toast } from 'sonner';
@@ -21,7 +22,6 @@ import {
Input,
} from '@/components/ui/input';
import { useUpdateUser } from '@/hooks/data/useUpdateUser';
import { useUserInfo } from '@/hooks/data/useUserInfo';
import { Switch } from '../ui/switch';
const formSchema = z.object({
@@ -31,9 +31,7 @@ const formSchema = z.object({
avatar: z.url().or(z.literal('')),
allow_public: z.boolean(),
});
export function EditProfileDialog() {
const { data } = useUserInfo();
const user = data.data!;
export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) {
const { mutateAsync } = useUpdateUser();
const form = useForm({

View File

@@ -0,0 +1,30 @@
import { Mail } from 'lucide-react';
import { Skeleton } from '../ui/skeleton';
export function ProfileError({ reason }: { reason: string }) {
return (
<div className="flex flex-col justify-center w-full lg:w-auto h-full lg:h-auto lg:flex-row lg:gap-8">
<div className="flex w-full flex-row mt-2 lg:mt-0 lg:flex-col lg:w-max">
<div className="flex flex-col w-full gap-3">
<div className="flex flex-col w-full gap-3">
<div className="flex flex-row gap-3 w-full lg:flex-col">
<Skeleton className="size-16 rounded-full border-2 border-muted lg:size-64" />
<div className="flex flex-1 flex-col justify-center lg:mt-3 gap-2">
<Skeleton className="w-32 h-8" />
<Skeleton className="w-20 h-6" />
</div>
</div>
<div className="flex flex-row gap-2 items-center text-sm px-1 lg:px-0">
<Mail className="h-4 w-4 stroke-muted-foreground" />
<Skeleton className="w-32 h-4" />
</div>
</div>
<Skeleton className="w-64 h-[40px]" />
</div>
</div>
<Skeleton 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 flex items-center justify-center">
{reason}
</Skeleton>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import { Mail } from 'lucide-react';
import { Skeleton } from '../ui/skeleton';
export function ProfileSkeleton() {
return (
<div className="flex flex-col justify-center w-full lg:w-auto h-full lg:h-auto lg:flex-row lg:gap-8">
<div className="flex w-full flex-row mt-2 lg:mt-0 lg:flex-col lg:w-max">
<div className="flex flex-col w-full gap-3">
<div className="flex flex-col w-full gap-3">
<div className="flex flex-row gap-3 w-full lg:flex-col">
<Skeleton className="size-16 rounded-full border-2 border-muted lg:size-64" />
<div className="flex flex-1 flex-col justify-center lg:mt-3 gap-2">
<Skeleton className="w-32 h-8" />
<Skeleton className="w-20 h-6" />
</div>
</div>
<div className="flex flex-row gap-2 items-center text-sm px-1 lg:px-0">
<Mail className="h-4 w-4 stroke-muted-foreground" />
<Skeleton className="w-32 h-4" />
</div>
</div>
<Skeleton className="w-64 h-[40px]" />
</div>
</div>
<Skeleton 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">
</Skeleton>
</div>
);
}

View File

@@ -1,21 +1,22 @@
import type { ServiceUserUserInfoData } from '@/client';
import { identicon } from '@dicebear/collection';
import { createAvatar } from '@dicebear/core';
import MDEditor from '@uiw/react-md-editor';
import { isNil } from 'lodash-es';
import {
isEmpty,
isNil,
} from 'lodash-es';
import { Mail, Pencil } from 'lucide-react';
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 { useUserInfo } from '@/hooks/data/useUserInfo';
import { base64ToUtf8, utf8ToBase64 } from '@/lib/utils';
import { Button } from '../ui/button';
import { EditProfileDialog } from './edit-profile-dialog';
export function MainProfile() {
const { data } = useUserInfo();
const user = data.data!;
export function Profile({ user }: { user: ServiceUserUserInfoData }) {
const [bio, setBio] = useState<string | undefined>(() => base64ToUtf8(user.bio ?? ''));
const [enableBioEdit, setEnableBioEdit] = useState(false);
const { mutateAsync } = useUpdateUser();
@@ -35,7 +36,7 @@ export function MainProfile() {
<div className="flex flex-col w-full gap-3">
<div className="flex flex-row gap-3 w-full lg:flex-col">
<Avatar className="size-16 rounded-full border-2 border-muted lg:size-64">
{user.avatar !== undefined ? <AvatarImage src={user.avatar} alt={user.nickname} /> : IdentIcon}
{!isEmpty(user.avatar) ? <AvatarImage src={user.avatar} alt={user.nickname} /> : IdentIcon}
</Avatar>
<div className="flex flex-1 flex-col justify-center lg:mt-3">
<span className="font-semibold text-2xl" aria-hidden="true">{user.nickname}</span>
@@ -47,19 +48,19 @@ export function MainProfile() {
{user.email}
</div>
</div>
<EditProfileDialog />
<EditProfileDialog user={user} />
</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">
{/* Bio */}
{enableBioEdit
? (
<MDEditor
value={bio}
onChange={setBio}
height="100%"
/>
)
<MDEditor
value={bio}
onChange={setBio}
height="100%"
/>
)
: <div className="p-6 prose dark:prose-invert"><Markdown>{bio}</Markdown></div>}
<Button
className="absolute bottom-4 right-4"