99 lines
3.8 KiB
TypeScript
99 lines
3.8 KiB
TypeScript
import type { ServiceUserUserInfoData } from '@/client';
|
|
import { identicon } from '@dicebear/collection';
|
|
import { createAvatar } from '@dicebear/core';
|
|
import MDEditor from '@uiw/react-md-editor';
|
|
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 { base64ToUtf8 } from '@/lib/utils';
|
|
import { Button } from '../ui/button';
|
|
import { Spinner } from '../ui/spinner';
|
|
import { EditProfileDialogContainer } from './edit-profile.dialog.container';
|
|
|
|
export function ProfileView({ user, onSaveBio }: { user: ServiceUserUserInfoData; onSaveBio: (bio: string) => Promise<void> }) {
|
|
const [bio, setBio] = useState<string | undefined>(() => base64ToUtf8(user.bio ?? ''));
|
|
const [enableBioEdit, setEnableBioEdit] = useState(false);
|
|
const [isSubmittingBio, setIsSubmittingBio] = useState(false);
|
|
|
|
const IdentIcon = useMemo(() => {
|
|
const avatar = createAvatar(identicon, {
|
|
size: 128,
|
|
seed: user.user_id,
|
|
}).toDataUri();
|
|
return <img src={avatar} alt="Avatar" />;
|
|
}, [user.user_id]);
|
|
|
|
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">
|
|
<Avatar className="size-16 rounded-full border-2 border-muted lg:size-64">
|
|
{!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>
|
|
<span className="text-[20px] text-muted-foreground" aria-hidden="true">{user.subtitle}</span>
|
|
</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" />
|
|
{user.email}
|
|
</div>
|
|
</div>
|
|
<EditProfileDialogContainer data={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%"
|
|
/>
|
|
)
|
|
: <div className="p-6 prose dark:prose-invert"><Markdown>{bio}</Markdown></div>}
|
|
<Button
|
|
className="absolute bottom-4 right-4"
|
|
// eslint-disable-next-line ts/no-misused-promises
|
|
onClick={async () => {
|
|
if (!enableBioEdit) {
|
|
setEnableBioEdit(true);
|
|
}
|
|
else {
|
|
if (!isNil(bio)) {
|
|
try {
|
|
setIsSubmittingBio(true);
|
|
await onSaveBio(bio);
|
|
setEnableBioEdit(false);
|
|
}
|
|
catch (error) {
|
|
console.error(error);
|
|
toast.error('个人简介更新失败');
|
|
}
|
|
finally {
|
|
setIsSubmittingBio(false);
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
size="icon-sm"
|
|
variant={enableBioEdit ? 'default' : 'outline'}
|
|
disabled={isSubmittingBio}
|
|
>
|
|
{isSubmittingBio ? <Spinner /> : <Pencil />}
|
|
</Button>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|