[rewrite] Added shadcn ui to frontend and slightly tweaked backend

This commit is contained in:
Thastertyn committed 2025-02-20 15:16:06 +01:00
1 parent 4c8817e853
commit 6e9334ba0e
202 files changed
+19385 -15424

No files matched your search

@@ -0,0 +1,217 @@
import { z } from 'zod'
import { format } from 'date-fns'
import { useForm } from 'react-hook-form'
import { CalendarIcon, CaretSortIcon, CheckIcon } from '@radix-ui/react-icons'
import { zodResolver } from '@hookform/resolvers/zod'
import { cn } from '@/lib/utils'
import { toast } from '@/hooks/use-toast'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
const languages = [
{ label: 'English', value: 'en' },
{ label: 'French', value: 'fr' },
{ label: 'German', value: 'de' },
{ label: 'Spanish', value: 'es' },
{ label: 'Portuguese', value: 'pt' },
{ label: 'Russian', value: 'ru' },
{ label: 'Japanese', value: 'ja' },
{ label: 'Korean', value: 'ko' },
{ label: 'Chinese', value: 'zh' },
] as const
const accountFormSchema = z.object({
name: z
.string()
.min(2, {
message: 'Name must be at least 2 characters.',
})
.max(30, {
message: 'Name must not be longer than 30 characters.',
}),
dob: z.date({
required_error: 'A date of birth is required.',
}),
language: z.string({
required_error: 'Please select a language.',
}),
})
type AccountFormValues = z.infer<typeof accountFormSchema>
// This can come from your database or API.
const defaultValues: Partial<AccountFormValues> = {
name: '',
}
export function AccountForm() {
const form = useForm<AccountFormValues>({
resolver: zodResolver(accountFormSchema),
defaultValues,
})
function onSubmit(data: AccountFormValues) {
toast({
title: 'You submitted the following values:',
description: (
<pre className='mt-2 w-[340px] rounded-md bg-slate-950 p-4'>
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder='Your name' {...field} />
</FormControl>
<FormDescription>
This is the name that will be displayed on your profile and in
emails.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='dob'
render={({ field }) => (
<FormItem className='flex flex-col'>
<FormLabel>Date of birth</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={'outline'}
className={cn(
'w-[240px] pl-3 text-left font-normal',
!field.value && 'text-muted-foreground'
)}
>
{field.value ? (
format(field.value, 'MMM d, yyyy')
) : (
<span>Pick a date</span>
)}
<CalendarIcon className='ml-auto h-4 w-4 opacity-50' />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className='w-auto p-0' align='start'>
<Calendar
mode='single'
selected={field.value}
onSelect={field.onChange}
disabled={(date: Date) =>
date > new Date() || date < new Date('1900-01-01')
}
/>
</PopoverContent>
</Popover>
<FormDescription>
Your date of birth is used to calculate your age.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='language'
render={({ field }) => (
<FormItem className='flex flex-col'>
<FormLabel>Language</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant='outline'
role='combobox'
className={cn(
'w-[200px] justify-between',
!field.value && 'text-muted-foreground'
)}
>
{field.value
? languages.find(
(language) => language.value === field.value
)?.label
: 'Select language'}
<CaretSortIcon className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className='w-[200px] p-0'>
<Command>
<CommandInput placeholder='Search language...' />
<CommandEmpty>No language found.</CommandEmpty>
<CommandGroup>
<CommandList>
{languages.map((language) => (
<CommandItem
value={language.label}
key={language.value}
onSelect={() => {
form.setValue('language', language.value)
}}
>
<CheckIcon
className={cn(
'mr-2 h-4 w-4',
language.value === field.value
? 'opacity-100'
: 'opacity-0'
)}
/>
{language.label}
</CommandItem>
))}
</CommandList>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
This is the language that will be used in the dashboard.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type='submit'>Update account</Button>
</form>
</Form>
)
}
@@ -0,0 +1,14 @@
import ContentSection from '../components/content-section'
import { AccountForm } from './account-form'
export default function SettingsAccount() {
return (
<ContentSection
title='Account'
desc='Update your account settings. Set your preferred language and
timezone.'
>
<AccountForm />
</ContentSection>
)
}
@@ -0,0 +1,173 @@
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { ChevronDownIcon } from '@radix-ui/react-icons'
import { zodResolver } from '@hookform/resolvers/zod'
import { fonts } from '@/config/fonts'
import { cn } from '@/lib/utils'
import { useFont } from '@/context/font-context'
import { useTheme } from '@/context/theme-context'
import { toast } from '@/hooks/use-toast'
import { Button, buttonVariants } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
const appearanceFormSchema = z.object({
theme: z.enum(['light', 'dark'], {
required_error: 'Please select a theme.',
}),
font: z.enum(fonts, {
invalid_type_error: 'Select a font',
required_error: 'Please select a font.',
}),
})
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>
export function AppearanceForm() {
const { font, setFont } = useFont()
const { theme, setTheme } = useTheme()
// This can come from your database or API.
const defaultValues: Partial<AppearanceFormValues> = {
theme: theme as 'light' | 'dark',
font,
}
const form = useForm<AppearanceFormValues>({
resolver: zodResolver(appearanceFormSchema),
defaultValues,
})
function onSubmit(data: AppearanceFormValues) {
if (data.font != font) setFont(data.font)
if (data.theme != theme) setTheme(data.theme)
toast({
title: 'You submitted the following values:',
description: (
<pre className='mt-2 w-[340px] rounded-md bg-slate-950 p-4'>
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
<FormField
control={form.control}
name='font'
render={({ field }) => (
<FormItem>
<FormLabel>Font</FormLabel>
<div className='relative w-max'>
<FormControl>
<select
className={cn(
buttonVariants({ variant: 'outline' }),
'w-[200px] appearance-none font-normal capitalize'
)}
{...field}
>
{fonts.map((font) => (
<option key={font} value={font}>
{font}
</option>
))}
</select>
</FormControl>
<ChevronDownIcon className='absolute right-3 top-2.5 h-4 w-4 opacity-50' />
</div>
<FormDescription className='font-manrope'>
Set the font you want to use in the dashboard.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='theme'
render={({ field }) => (
<FormItem className='space-y-1'>
<FormLabel>Theme</FormLabel>
<FormDescription>
Select the theme for the dashboard.
</FormDescription>
<FormMessage />
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className='grid max-w-md grid-cols-2 gap-8 pt-2'
>
<FormItem>
<FormLabel className='[&:has([data-state=checked])>div]:border-primary'>
<FormControl>
<RadioGroupItem value='light' className='sr-only' />
</FormControl>
<div className='items-center rounded-md border-2 border-muted p-1 hover:border-accent'>
<div className='space-y-2 rounded-sm bg-[#ecedef] p-2'>
<div className='space-y-2 rounded-md bg-white p-2 shadow-sm'>
<div className='h-2 w-[80px] rounded-lg bg-[#ecedef]' />
<div className='h-2 w-[100px] rounded-lg bg-[#ecedef]' />
</div>
<div className='flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm'>
<div className='h-4 w-4 rounded-full bg-[#ecedef]' />
<div className='h-2 w-[100px] rounded-lg bg-[#ecedef]' />
</div>
<div className='flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm'>
<div className='h-4 w-4 rounded-full bg-[#ecedef]' />
<div className='h-2 w-[100px] rounded-lg bg-[#ecedef]' />
</div>
</div>
</div>
<span className='block w-full p-2 text-center font-normal'>
Light
</span>
</FormLabel>
</FormItem>
<FormItem>
<FormLabel className='[&:has([data-state=checked])>div]:border-primary'>
<FormControl>
<RadioGroupItem value='dark' className='sr-only' />
</FormControl>
<div className='items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground'>
<div className='space-y-2 rounded-sm bg-slate-950 p-2'>
<div className='space-y-2 rounded-md bg-slate-800 p-2 shadow-sm'>
<div className='h-2 w-[80px] rounded-lg bg-slate-400' />
<div className='h-2 w-[100px] rounded-lg bg-slate-400' />
</div>
<div className='flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm'>
<div className='h-4 w-4 rounded-full bg-slate-400' />
<div className='h-2 w-[100px] rounded-lg bg-slate-400' />
</div>
<div className='flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm'>
<div className='h-4 w-4 rounded-full bg-slate-400' />
<div className='h-2 w-[100px] rounded-lg bg-slate-400' />
</div>
</div>
</div>
<span className='block w-full p-2 text-center font-normal'>
Dark
</span>
</FormLabel>
</FormItem>
</RadioGroup>
</FormItem>
)}
/>
<Button type='submit'>Update preferences</Button>
</form>
</Form>
)
}
@@ -0,0 +1,14 @@
import ContentSection from '../components/content-section'
import { AppearanceForm } from './appearance-form'
export default function SettingsAppearance() {
return (
<ContentSection
title='Appearance'
desc='Customize the appearance of the app. Automatically switch between day
and night themes.'
>
<AppearanceForm />
</ContentSection>
)
}
@@ -0,0 +1,27 @@
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
interface ContentSectionProps {
title: string
desc: string
children: React.JSX.Element
}
export default function ContentSection({
title,
desc,
children,
}: ContentSectionProps) {
return (
<div className='flex flex-1 flex-col'>
<div className='flex-none'>
<h3 className='text-lg font-medium'>{title}</h3>
<p className='text-sm text-muted-foreground'>{desc}</p>
</div>
<Separator className='my-4 flex-none' />
<ScrollArea className='faded-bottom -mx-4 flex-1 scroll-smooth px-4 md:pb-16'>
<div className='-mx-1 px-1.5 lg:max-w-xl'>{children}</div>
</ScrollArea>
</div>
)
}
@@ -0,0 +1,89 @@
import { useState, type JSX } from 'react'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { Link } from '@tanstack/react-router'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
items: {
href: string
title: string
icon: JSX.Element
}[]
}
export default function SidebarNav({
className,
items,
...props
}: SidebarNavProps) {
const { pathname } = useLocation()
const navigate = useNavigate()
const [val, setVal] = useState(pathname ?? '/settings')
const handleSelect = (e: string) => {
setVal(e)
navigate({ to: e })
}
return (
<>
<div className='p-1 md:hidden'>
<Select value={val} onValueChange={handleSelect}>
<SelectTrigger className='h-12 sm:w-48'>
<SelectValue placeholder='Theme' />
</SelectTrigger>
<SelectContent>
{items.map((item) => (
<SelectItem key={item.href} value={item.href}>
<div className='flex gap-x-4 px-2 py-1'>
<span className='scale-125'>{item.icon}</span>
<span className='text-md'>{item.title}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<ScrollArea
orientation='horizontal'
type='always'
className='hidden w-full min-w-40 bg-background px-1 py-2 md:block'
>
<nav
className={cn(
'flex space-x-2 py-1 lg:flex-col lg:space-x-0 lg:space-y-1',
className
)}
{...props}
>
{items.map((item) => (
<Link
key={item.href}
to={item.href}
className={cn(
buttonVariants({ variant: 'ghost' }),
pathname === item.href
? 'bg-muted hover:bg-muted'
: 'hover:bg-transparent hover:underline',
'justify-start'
)}
>
<span className='mr-2'>{item.icon}</span>
{item.title}
</Link>
))}
</nav>
</ScrollArea>
</>
)
}
@@ -0,0 +1,129 @@
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { toast } from '@/hooks/use-toast'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
const items = [
{
id: 'recents',
label: 'Recents',
},
{
id: 'home',
label: 'Home',
},
{
id: 'applications',
label: 'Applications',
},
{
id: 'desktop',
label: 'Desktop',
},
{
id: 'downloads',
label: 'Downloads',
},
{
id: 'documents',
label: 'Documents',
},
] as const
const displayFormSchema = z.object({
items: z.array(z.string()).refine((value) => value.some((item) => item), {
message: 'You have to select at least one item.',
}),
})
type DisplayFormValues = z.infer<typeof displayFormSchema>
// This can come from your database or API.
const defaultValues: Partial<DisplayFormValues> = {
items: ['recents', 'home'],
}
export function DisplayForm() {
const form = useForm<DisplayFormValues>({
resolver: zodResolver(displayFormSchema),
defaultValues,
})
function onSubmit(data: DisplayFormValues) {
toast({
title: 'You submitted the following values:',
description: (
<pre className='mt-2 w-[340px] rounded-md bg-slate-950 p-4'>
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
<FormField
control={form.control}
name='items'
render={() => (
<FormItem>
<div className='mb-4'>
<FormLabel className='text-base'>Sidebar</FormLabel>
<FormDescription>
Select the items you want to display in the sidebar.
</FormDescription>
</div>
{items.map((item) => (
<FormField
key={item.id}
control={form.control}
name='items'
render={({ field }) => {
return (
<FormItem
key={item.id}
className='flex flex-row items-start space-x-3 space-y-0'
>
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, item.id])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
)
}}
/>
</FormControl>
<FormLabel className='font-normal'>
{item.label}
</FormLabel>
</FormItem>
)
}}
/>
))}
<FormMessage />
</FormItem>
)}
/>
<Button type='submit'>Update display</Button>
</form>
</Form>
)
}
@@ -0,0 +1,13 @@
import ContentSection from '../components/content-section'
import { DisplayForm } from './display-form'
export default function SettingsDisplay() {
return (
<ContentSection
title='Display'
desc="Turn items on or off to control what's displayed in the app."
>
<DisplayForm />
</ContentSection>
)
}
+78
View File
@@ -0,0 +1,78 @@
import { Outlet } from '@tanstack/react-router'
import {
IconBrowserCheck,
IconNotification,
IconPalette,
IconTool,
IconUser,
} from '@tabler/icons-react'
import { Separator } from '@/components/ui/separator'
import { Header } from '@/components/layout/header'
import { Main } from '@/components/layout/main'
import { ProfileDropdown } from '@/components/profile-dropdown'
import { Search } from '@/components/search'
import { ThemeSwitch } from '@/components/theme-switch'
import SidebarNav from './components/sidebar-nav'
export default function Settings() {
return (
<>
{/* ===== Top Heading ===== */}
<Header>
<Search />
<div className='ml-auto flex items-center space-x-4'>
<ThemeSwitch />
<ProfileDropdown />
</div>
</Header>
<Main fixed>
<div className='space-y-0.5'>
<h1 className='text-2xl font-bold tracking-tight md:text-3xl'>
Settings
</h1>
<p className='text-muted-foreground'>
Manage your account settings and set e-mail preferences.
</p>
</div>
<Separator className='my-4 lg:my-6' />
<div className='flex flex-1 flex-col space-y-2 overflow-hidden md:space-y-2 lg:flex-row lg:space-x-12 lg:space-y-0'>
<aside className='top-0 lg:sticky lg:w-1/5'>
<SidebarNav items={sidebarNavItems} />
</aside>
<div className='flex w-full overflow-y-hidden p-1 pr-4'>
<Outlet />
</div>
</div>
</Main>
</>
)
}
const sidebarNavItems = [
{
title: 'Profile',
icon: <IconUser size={18} />,
href: '/settings',
},
{
title: 'Account',
icon: <IconTool size={18} />,
href: '/settings/account',
},
{
title: 'Appearance',
icon: <IconPalette size={18} />,
href: '/settings/appearance',
},
{
title: 'Notifications',
icon: <IconNotification size={18} />,
href: '/settings/notifications',
},
{
title: 'Display',
icon: <IconBrowserCheck size={18} />,
href: '/settings/display',
},
]
@@ -0,0 +1,13 @@
import ContentSection from '../components/content-section'
import { NotificationsForm } from './notifications-form'
export default function SettingsNotifications() {
return (
<ContentSection
title='Notifications'
desc='Configure how you receive notifications.'
>
<NotificationsForm />
</ContentSection>
)
}
@@ -0,0 +1,225 @@
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Link } from '@tanstack/react-router'
import { toast } from '@/hooks/use-toast'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Switch } from '@/components/ui/switch'
const notificationsFormSchema = z.object({
type: z.enum(['all', 'mentions', 'none'], {
required_error: 'You need to select a notification type.',
}),
mobile: z.boolean().default(false).optional(),
communication_emails: z.boolean().default(false).optional(),
social_emails: z.boolean().default(false).optional(),
marketing_emails: z.boolean().default(false).optional(),
security_emails: z.boolean(),
})
type NotificationsFormValues = z.infer<typeof notificationsFormSchema>
// This can come from your database or API.
const defaultValues: Partial<NotificationsFormValues> = {
communication_emails: false,
marketing_emails: false,
social_emails: true,
security_emails: true,
}
export function NotificationsForm() {
const form = useForm<NotificationsFormValues>({
resolver: zodResolver(notificationsFormSchema),
defaultValues,
})
function onSubmit(data: NotificationsFormValues) {
toast({
title: 'You submitted the following values:',
description: (
<pre className='mt-2 w-[340px] rounded-md bg-slate-950 p-4'>
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
<FormField
control={form.control}
name='type'
render={({ field }) => (
<FormItem className='relative space-y-3'>
<FormLabel>Notify me about...</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className='flex flex-col space-y-1'
>
<FormItem className='flex items-center space-x-3 space-y-0'>
<FormControl>
<RadioGroupItem value='all' />
</FormControl>
<FormLabel className='font-normal'>
All new messages
</FormLabel>
</FormItem>
<FormItem className='flex items-center space-x-3 space-y-0'>
<FormControl>
<RadioGroupItem value='mentions' />
</FormControl>
<FormLabel className='font-normal'>
Direct messages and mentions
</FormLabel>
</FormItem>
<FormItem className='flex items-center space-x-3 space-y-0'>
<FormControl>
<RadioGroupItem value='none' />
</FormControl>
<FormLabel className='font-normal'>Nothing</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className='relative'>
<h3 className='mb-4 text-lg font-medium'>Email Notifications</h3>
<div className='space-y-4'>
<FormField
control={form.control}
name='communication_emails'
render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
<div className='space-y-0.5'>
<FormLabel className='text-base'>
Communication emails
</FormLabel>
<FormDescription>
Receive emails about your account activity.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='marketing_emails'
render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
<div className='space-y-0.5'>
<FormLabel className='text-base'>
Marketing emails
</FormLabel>
<FormDescription>
Receive emails about new products, features, and more.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='social_emails'
render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
<div className='space-y-0.5'>
<FormLabel className='text-base'>Social emails</FormLabel>
<FormDescription>
Receive emails for friend requests, follows, and more.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='security_emails'
render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
<div className='space-y-0.5'>
<FormLabel className='text-base'>Security emails</FormLabel>
<FormDescription>
Receive emails about your account activity and security.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled
aria-readonly
/>
</FormControl>
</FormItem>
)}
/>
</div>
</div>
<FormField
control={form.control}
name='mobile'
render={({ field }) => (
<FormItem className='relative flex flex-row items-start space-x-3 space-y-0'>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className='space-y-1 leading-none'>
<FormLabel>
Use different settings for my mobile devices
</FormLabel>
<FormDescription>
You can manage your mobile notifications in the{' '}
<Link
to='/settings'
className='underline decoration-dashed underline-offset-4 hover:decoration-solid'
>
mobile settings
</Link>{' '}
page.
</FormDescription>
</div>
</FormItem>
)}
/>
<Button type='submit'>Update notifications</Button>
</form>
</Form>
)
}
@@ -0,0 +1,13 @@
import ContentSection from '../components/content-section'
import ProfileForm from './profile-form'
export default function SettingsProfile() {
return (
<ContentSection
title='Profile'
desc='This is how others will see you on the site.'
>
<ProfileForm />
</ContentSection>
)
}
@@ -0,0 +1,188 @@
import { z } from 'zod'
import { useFieldArray, useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Link } from '@tanstack/react-router'
import { cn } from '@/lib/utils'
import { toast } from '@/hooks/use-toast'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
const profileFormSchema = z.object({
username: z
.string()
.min(2, {
message: 'Username must be at least 2 characters.',
})
.max(30, {
message: 'Username must not be longer than 30 characters.',
}),
email: z
.string({
required_error: 'Please select an email to display.',
})
.email(),
bio: z.string().max(160).min(4),
urls: z
.array(
z.object({
value: z.string().url({ message: 'Please enter a valid URL.' }),
})
)
.optional(),
})
type ProfileFormValues = z.infer<typeof profileFormSchema>
// This can come from your database or API.
const defaultValues: Partial<ProfileFormValues> = {
bio: 'I own a computer.',
urls: [
{ value: 'https://shadcn.com' },
{ value: 'http://twitter.com/shadcn' },
],
}
export default function ProfileForm() {
const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileFormSchema),
defaultValues,
mode: 'onChange',
})
const { fields, append } = useFieldArray({
name: 'urls',
control: form.control,
})
function onSubmit(data: ProfileFormValues) {
toast({
title: 'You submitted the following values:',
description: (
<pre className='mt-2 w-[340px] rounded-md bg-slate-950 p-4'>
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
<FormField
control={form.control}
name='username'
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder='shadcn' {...field} />
</FormControl>
<FormDescription>
This is your public display name. It can be your real name or a
pseudonym. You can only change this once every 30 days.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='email'
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder='Select a verified email to display' />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='m@example.com'>m@example.com</SelectItem>
<SelectItem value='m@google.com'>m@google.com</SelectItem>
<SelectItem value='m@support.com'>m@support.com</SelectItem>
</SelectContent>
</Select>
<FormDescription>
You can manage verified email addresses in your{' '}
<Link to='/'>email settings</Link>.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='bio'
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder='Tell us a little bit about yourself'
className='resize-none'
{...field}
/>
</FormControl>
<FormDescription>
You can <span>@mention</span> other users and organizations to
link to them.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div>
{fields.map((field, index) => (
<FormField
control={form.control}
key={field.id}
name={`urls.${index}.value`}
render={({ field }) => (
<FormItem>
<FormLabel className={cn(index !== 0 && 'sr-only')}>
URLs
</FormLabel>
<FormDescription className={cn(index !== 0 && 'sr-only')}>
Add links to your website, blog, or social media profiles.
</FormDescription>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
))}
<Button
type='button'
variant='outline'
size='sm'
className='mt-2'
onClick={() => append({ value: '' })}
>
Add URL
</Button>
</div>
<Button type='submit'>Update profile</Button>
</form>
</Form>
)
}