feat(client): add storybook and workbench profile flow
Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
@@ -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({
|
||||
|
||||
30
client/cms/src/components/profile/profile.error.tsx
Normal file
30
client/cms/src/components/profile/profile.error.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
29
client/cms/src/components/profile/profile.skeleton.tsx
Normal file
29
client/cms/src/components/profile/profile.skeleton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
IconDotsVertical,
|
||||
IconLogout,
|
||||
} from '@tabler/icons-react';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
@@ -53,7 +54,7 @@ function NavUser_() {
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
{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="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.nickname}</span>
|
||||
@@ -73,7 +74,7 @@ function NavUser_() {
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
{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="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.nickname}</span>
|
||||
|
||||
39
client/cms/src/components/workbenchCards/event-card.tsx
Normal file
39
client/cms/src/components/workbenchCards/event-card.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
export function EventCard({ type, coverImage, eventName, description, startTime, endTime }:
|
||||
{
|
||||
type: 'official' | 'party',
|
||||
coverImage: string, eventName: string, description: string, startTime: string, endTime: string
|
||||
}) {
|
||||
return (
|
||||
<Card className="relative mx-auto w-full max-w-sm pt-0">
|
||||
<div className="absolute inset-0 z-30 aspect-video bg-black/35" />
|
||||
<img
|
||||
src="https://avatar.vercel.sh/shadcn1"
|
||||
alt="Event cover"
|
||||
className="relative z-20 aspect-video w-full object-cover brightness-60 grayscale dark:brightness-40"
|
||||
/>
|
||||
<CardHeader>
|
||||
<CardAction>
|
||||
<Badge variant="secondary">Featured</Badge>
|
||||
</CardAction>
|
||||
<CardTitle>Design systems meetup</CardTitle>
|
||||
<CardDescription>
|
||||
A practical talk on component APIs, accessibility, and shipping
|
||||
faster.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Button className="w-full">View Event</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user