doo-boilerplate 0.2.11 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doo-boilerplate",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "CLI to scaffold Pila portal frontend projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,6 +56,7 @@
56
56
  "@radix-ui/react-slot": "^1.1.2",
57
57
  "@radix-ui/react-switch": "^1.1.3",
58
58
  "@radix-ui/react-tabs": "^1.1.3",
59
+ "@radix-ui/react-radio-group": "^1.2.3",
59
60
  "@radix-ui/react-tooltip": "^1.1.8",
60
61
  "cmdk": "^1.1.1",
61
62
  "recharts": "^2.15.0",
@@ -1,6 +1,6 @@
1
1
  import { useState } from 'react'
2
2
  import { Link } from '@tanstack/react-router'
3
- import { Bell, LayoutDashboard, LayoutTemplate, Settings, Table2, User, Users, ChevronLeft, ChevronRight } from 'lucide-react'
3
+ import { Bell, LayoutDashboard, LayoutTemplate, Settings, Table2, Users, ChevronLeft, ChevronRight } from 'lucide-react'
4
4
 
5
5
  import { cn } from '@/lib/utils'
6
6
  import { Button } from '@/components/ui/button'
@@ -11,7 +11,6 @@ import { siteConfig } from '@/config/site'
11
11
  const navItems = [
12
12
  { to: '/dashboard', label: 'Dashboard', icon: LayoutDashboard },
13
13
  { to: '/users', label: 'Users', icon: Users },
14
- { to: '/profile', label: 'Profile', icon: User },
15
14
  { to: '/settings', label: 'Settings', icon: Settings },
16
15
  ] as const
17
16
 
@@ -0,0 +1,34 @@
1
+ import * as React from 'react'
2
+ import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
3
+ import { Circle } from 'lucide-react'
4
+
5
+ import { cn } from '@/lib/utils'
6
+
7
+ const RadioGroup = React.forwardRef<
8
+ React.ElementRef<typeof RadioGroupPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <RadioGroupPrimitive.Root ref={ref} className={cn('grid gap-2', className)} {...props} />
12
+ ))
13
+ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
14
+
15
+ const RadioGroupItem = React.forwardRef<
16
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
17
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
18
+ >(({ className, ...props }, ref) => (
19
+ <RadioGroupPrimitive.Item
20
+ ref={ref}
21
+ className={cn(
22
+ 'aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
23
+ className
24
+ )}
25
+ {...props}
26
+ >
27
+ <RadioGroupPrimitive.Indicator className='flex items-center justify-center'>
28
+ <Circle className='h-2.5 w-2.5 fill-current text-current' />
29
+ </RadioGroupPrimitive.Indicator>
30
+ </RadioGroupPrimitive.Item>
31
+ ))
32
+ RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
33
+
34
+ export { RadioGroup, RadioGroupItem }
@@ -0,0 +1,19 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <textarea
8
+ ref={ref}
9
+ className={cn(
10
+ 'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ )
17
+ Textarea.displayName = 'Textarea'
18
+
19
+ export { Textarea }
@@ -0,0 +1,142 @@
1
+ import { useState } from 'react'
2
+ import { useForm } from 'react-hook-form'
3
+ import { zodResolver } from '@hookform/resolvers/zod'
4
+ import { z } from 'zod'
5
+ import { Check, ChevronsUpDown } from 'lucide-react'
6
+ import { toast } from 'sonner'
7
+
8
+ import { cn } from '@/lib/utils'
9
+ import { Button } from '@/components/ui/button'
10
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
11
+ import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
12
+ import { Input } from '@/components/ui/input'
13
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
14
+
15
+ const LANGUAGES = [
16
+ { label: 'English', value: 'en' },
17
+ { label: 'French', value: 'fr' },
18
+ { label: 'German', value: 'de' },
19
+ { label: 'Spanish', value: 'es' },
20
+ { label: 'Portuguese', value: 'pt' },
21
+ { label: 'Russian', value: 'ru' },
22
+ { label: 'Japanese', value: 'ja' },
23
+ { label: 'Korean', value: 'ko' },
24
+ { label: 'Chinese', value: 'zh' },
25
+ ] as const
26
+
27
+ const accountSchema = z.object({
28
+ name: z.string().min(2, 'At least 2 characters').max(30, 'Max 30 characters'),
29
+ dob: z.string().optional(),
30
+ language: z.string({ required_error: 'Please select a language.' }),
31
+ })
32
+
33
+ type AccountValues = z.infer<typeof accountSchema>
34
+
35
+ export function AccountSettingsForm() {
36
+ const [langOpen, setLangOpen] = useState(false)
37
+
38
+ const form = useForm<AccountValues>({
39
+ resolver: zodResolver(accountSchema),
40
+ defaultValues: { name: '', dob: '', language: 'en' },
41
+ })
42
+
43
+ function onSubmit(data: AccountValues) {
44
+ toast.success('Account updated successfully')
45
+ console.log(data)
46
+ }
47
+
48
+ return (
49
+ <Form {...form}>
50
+ <form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
51
+ <FormField
52
+ control={form.control}
53
+ name='name'
54
+ render={({ field }) => (
55
+ <FormItem>
56
+ <FormLabel>Name</FormLabel>
57
+ <FormControl>
58
+ <Input placeholder='Your name' {...field} />
59
+ </FormControl>
60
+ <FormDescription>
61
+ This is the name that will be displayed on your profile and in emails.
62
+ </FormDescription>
63
+ <FormMessage />
64
+ </FormItem>
65
+ )}
66
+ />
67
+
68
+ <FormField
69
+ control={form.control}
70
+ name='dob'
71
+ render={({ field }) => (
72
+ <FormItem>
73
+ <FormLabel>Date of birth</FormLabel>
74
+ <FormControl>
75
+ <Input type='date' className='w-[240px]' {...field} />
76
+ </FormControl>
77
+ <FormDescription>
78
+ Your date of birth is used to calculate your age.
79
+ </FormDescription>
80
+ <FormMessage />
81
+ </FormItem>
82
+ )}
83
+ />
84
+
85
+ <FormField
86
+ control={form.control}
87
+ name='language'
88
+ render={({ field }) => (
89
+ <FormItem className='flex flex-col'>
90
+ <FormLabel>Language</FormLabel>
91
+ <Popover open={langOpen} onOpenChange={setLangOpen}>
92
+ <PopoverTrigger asChild>
93
+ <FormControl>
94
+ <Button
95
+ variant='outline'
96
+ role='combobox'
97
+ className={cn('w-[240px] justify-between', !field.value && 'text-muted-foreground')}
98
+ >
99
+ {field.value
100
+ ? LANGUAGES.find((l) => l.value === field.value)?.label
101
+ : 'Select language'}
102
+ <ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
103
+ </Button>
104
+ </FormControl>
105
+ </PopoverTrigger>
106
+ <PopoverContent className='w-[240px] p-0'>
107
+ <Command>
108
+ <CommandInput placeholder='Search language...' />
109
+ <CommandList>
110
+ <CommandEmpty>No language found.</CommandEmpty>
111
+ <CommandGroup>
112
+ {LANGUAGES.map((lang) => (
113
+ <CommandItem
114
+ key={lang.value}
115
+ value={lang.value}
116
+ onSelect={() => {
117
+ form.setValue('language', lang.value)
118
+ setLangOpen(false)
119
+ }}
120
+ >
121
+ <Check className={cn('mr-2 h-4 w-4', field.value === lang.value ? 'opacity-100' : 'opacity-0')} />
122
+ {lang.label}
123
+ </CommandItem>
124
+ ))}
125
+ </CommandGroup>
126
+ </CommandList>
127
+ </Command>
128
+ </PopoverContent>
129
+ </Popover>
130
+ <FormDescription>
131
+ This is the language that will be used in the dashboard.
132
+ </FormDescription>
133
+ <FormMessage />
134
+ </FormItem>
135
+ )}
136
+ />
137
+
138
+ <Button type='submit'>Update account</Button>
139
+ </form>
140
+ </Form>
141
+ )
142
+ }
@@ -0,0 +1,129 @@
1
+ import { useForm } from 'react-hook-form'
2
+ import { zodResolver } from '@hookform/resolvers/zod'
3
+ import { z } from 'zod'
4
+ import { useTheme } from 'next-themes'
5
+ import { toast } from 'sonner'
6
+
7
+ import { cn } from '@/lib/utils'
8
+ import { Button } from '@/components/ui/button'
9
+ import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
10
+ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
12
+
13
+ const FONTS = [
14
+ { label: 'Inter', value: 'inter' },
15
+ { label: 'Manrope', value: 'manrope' },
16
+ { label: 'System', value: 'system' },
17
+ ] as const
18
+
19
+ const appearanceSchema = z.object({
20
+ theme: z.enum(['light', 'dark', 'system']),
21
+ font: z.enum(['inter', 'manrope', 'system']),
22
+ })
23
+
24
+ type AppearanceValues = z.infer<typeof appearanceSchema>
25
+
26
+ /** Theme preview card for light/dark/system radio options */
27
+ function ThemeCard({ mode, label }: { mode: string; label: string }) {
28
+ const isDark = mode === 'dark'
29
+ const bg = isDark ? 'bg-zinc-950' : 'bg-white'
30
+ const border = isDark ? 'border-zinc-700' : 'border-zinc-200'
31
+ const cardBg = isDark ? 'bg-zinc-900' : 'bg-zinc-50'
32
+ const lineBg = isDark ? 'bg-zinc-700' : 'bg-zinc-200'
33
+ const accentBg = isDark ? 'bg-zinc-700' : 'bg-zinc-300'
34
+ const textColor = isDark ? 'text-zinc-100' : 'text-zinc-900'
35
+
36
+ return (
37
+ <div className={cn('rounded-md border-2 p-2', border, bg, 'space-y-2')}>
38
+ <div className={cn('rounded-sm border p-2 shadow-sm space-y-2', cardBg, border)}>
39
+ <div className={cn('h-2 w-[80px] rounded-lg', accentBg)} />
40
+ <div className={cn('h-2 w-[100px] rounded-lg', lineBg)} />
41
+ <div className={cn('h-2 w-[60px] rounded-lg', lineBg)} />
42
+ </div>
43
+ <span className={cn('text-xs font-medium', textColor)}>{label}</span>
44
+ </div>
45
+ )
46
+ }
47
+
48
+ export function AppearanceSettingsForm() {
49
+ const { theme: currentTheme, setTheme } = useTheme()
50
+
51
+ const form = useForm<AppearanceValues>({
52
+ resolver: zodResolver(appearanceSchema),
53
+ defaultValues: {
54
+ theme: (currentTheme as AppearanceValues['theme']) ?? 'system',
55
+ font: 'inter',
56
+ },
57
+ })
58
+
59
+ function onSubmit(data: AppearanceValues) {
60
+ setTheme(data.theme)
61
+ toast.success('Appearance updated')
62
+ }
63
+
64
+ return (
65
+ <Form {...form}>
66
+ <form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
67
+ <FormField
68
+ control={form.control}
69
+ name='font'
70
+ render={({ field }) => (
71
+ <FormItem>
72
+ <FormLabel>Font</FormLabel>
73
+ <FormDescription>Set the font you want to use in the dashboard.</FormDescription>
74
+ <FormControl>
75
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
76
+ <SelectTrigger className='w-[200px]'>
77
+ <SelectValue placeholder='Select font' />
78
+ </SelectTrigger>
79
+ <SelectContent>
80
+ {FONTS.map((f) => (
81
+ <SelectItem key={f.value} value={f.value}>
82
+ {f.label}
83
+ </SelectItem>
84
+ ))}
85
+ </SelectContent>
86
+ </Select>
87
+ </FormControl>
88
+ <FormMessage />
89
+ </FormItem>
90
+ )}
91
+ />
92
+
93
+ <FormField
94
+ control={form.control}
95
+ name='theme'
96
+ render={({ field }) => (
97
+ <FormItem className='space-y-1'>
98
+ <FormLabel>Theme</FormLabel>
99
+ <FormDescription>Select the theme for the dashboard.</FormDescription>
100
+ <FormMessage />
101
+ <RadioGroup
102
+ onValueChange={field.onChange}
103
+ defaultValue={field.value}
104
+ className='grid grid-cols-3 gap-4 pt-2 max-w-md'
105
+ >
106
+ {([
107
+ { value: 'light', label: 'Light' },
108
+ { value: 'dark', label: 'Dark' },
109
+ { value: 'system', label: 'System' },
110
+ ] as const).map(({ value, label }) => (
111
+ <FormItem key={value}>
112
+ <FormLabel className='cursor-pointer [&:has([data-state=checked])>div]:border-primary'>
113
+ <FormControl>
114
+ <RadioGroupItem value={value} className='sr-only' />
115
+ </FormControl>
116
+ <ThemeCard mode={value === 'system' ? 'light' : value} label={label} />
117
+ </FormLabel>
118
+ </FormItem>
119
+ ))}
120
+ </RadioGroup>
121
+ </FormItem>
122
+ )}
123
+ />
124
+
125
+ <Button type='submit'>Update preferences</Button>
126
+ </form>
127
+ </Form>
128
+ )
129
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import { Separator } from '@/components/ui/separator'
4
+
5
+ interface SettingsContentSectionProps {
6
+ title: string
7
+ desc: string
8
+ children: ReactNode
9
+ }
10
+
11
+ export function SettingsContentSection({ title, desc, children }: SettingsContentSectionProps) {
12
+ return (
13
+ <div className='flex flex-1 flex-col'>
14
+ <div>
15
+ <h3 className='text-lg font-medium'>{title}</h3>
16
+ <p className='text-sm text-muted-foreground'>{desc}</p>
17
+ </div>
18
+ <Separator className='my-4' />
19
+ <div className='flex-1'>{children}</div>
20
+ </div>
21
+ )
22
+ }
@@ -0,0 +1,62 @@
1
+ import type { JSX } from 'react'
2
+ import { Link, useLocation, useNavigate } from '@tanstack/react-router'
3
+
4
+ import { cn } from '@/lib/utils'
5
+ import { buttonVariants } from '@/components/ui/button'
6
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
7
+
8
+ interface NavItem {
9
+ href: string
10
+ title: string
11
+ icon: JSX.Element
12
+ }
13
+
14
+ interface SettingsSidebarNavProps extends React.HTMLAttributes<HTMLElement> {
15
+ items: NavItem[]
16
+ }
17
+
18
+ export function SettingsSidebarNav({ className, items, ...props }: SettingsSidebarNavProps) {
19
+ const { pathname } = useLocation()
20
+ const navigate = useNavigate()
21
+
22
+ return (
23
+ <>
24
+ {/* Mobile: dropdown select */}
25
+ <div className='p-1 md:hidden'>
26
+ <Select value={pathname} onValueChange={(v) => navigate({ to: v })}>
27
+ <SelectTrigger className='h-12 sm:w-48'>
28
+ <SelectValue placeholder='Select section' />
29
+ </SelectTrigger>
30
+ <SelectContent>
31
+ {items.map((item) => (
32
+ <SelectItem key={item.href} value={item.href}>
33
+ <div className='flex gap-x-4 px-2 py-1'>
34
+ <span className='scale-125'>{item.icon}</span>
35
+ <span>{item.title}</span>
36
+ </div>
37
+ </SelectItem>
38
+ ))}
39
+ </SelectContent>
40
+ </Select>
41
+ </div>
42
+
43
+ {/* Desktop: vertical nav */}
44
+ <nav className={cn('hidden md:flex md:flex-col md:space-y-1', className)} {...props}>
45
+ {items.map((item) => (
46
+ <Link
47
+ key={item.href}
48
+ to={item.href}
49
+ className={cn(
50
+ buttonVariants({ variant: 'ghost' }),
51
+ pathname === item.href ? 'bg-muted hover:bg-muted' : 'hover:bg-muted/50',
52
+ 'justify-start gap-2'
53
+ )}
54
+ >
55
+ {item.icon}
56
+ {item.title}
57
+ </Link>
58
+ ))}
59
+ </nav>
60
+ </>
61
+ )
62
+ }
@@ -0,0 +1,139 @@
1
+ import { useFieldArray, useForm } from 'react-hook-form'
2
+ import { zodResolver } from '@hookform/resolvers/zod'
3
+ import { z } from 'zod'
4
+ import { toast } from 'sonner'
5
+
6
+ import { useAuthStore } from '@/stores/auth-store'
7
+ import { Button } from '@/components/ui/button'
8
+ import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
9
+ import { Input } from '@/components/ui/input'
10
+ import { Textarea } from '@/components/ui/textarea'
11
+
12
+ const profileSchema = z.object({
13
+ username: z.string().min(2, 'At least 2 characters').max(30, 'Max 30 characters'),
14
+ email: z.string().email('Invalid email'),
15
+ bio: z.string().min(4, 'At least 4 characters').max(160, 'Max 160 characters').optional().or(z.literal('')),
16
+ urls: z.array(z.object({ value: z.string().url('Invalid URL').optional().or(z.literal('')) })).max(5),
17
+ })
18
+
19
+ type ProfileValues = z.infer<typeof profileSchema>
20
+
21
+ export function ProfileSettingsForm() {
22
+ const { user } = useAuthStore()
23
+
24
+ const form = useForm<ProfileValues>({
25
+ resolver: zodResolver(profileSchema),
26
+ defaultValues: {
27
+ username: user?.name ?? '',
28
+ email: user?.email ?? '',
29
+ bio: '',
30
+ urls: [{ value: '' }],
31
+ },
32
+ mode: 'onChange',
33
+ })
34
+
35
+ const { fields, append, remove } = useFieldArray({ name: 'urls', control: form.control })
36
+
37
+ function onSubmit(data: ProfileValues) {
38
+ toast.success('Profile updated successfully')
39
+ console.log(data)
40
+ }
41
+
42
+ return (
43
+ <Form {...form}>
44
+ <form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
45
+ <FormField
46
+ control={form.control}
47
+ name='username'
48
+ render={({ field }) => (
49
+ <FormItem>
50
+ <FormLabel>Username</FormLabel>
51
+ <FormControl>
52
+ <Input placeholder='Your name' {...field} />
53
+ </FormControl>
54
+ <FormDescription>
55
+ This is your public display name. It can be your real name or a pseudonym.
56
+ </FormDescription>
57
+ <FormMessage />
58
+ </FormItem>
59
+ )}
60
+ />
61
+
62
+ <FormField
63
+ control={form.control}
64
+ name='email'
65
+ render={({ field }) => (
66
+ <FormItem>
67
+ <FormLabel>Email</FormLabel>
68
+ <FormControl>
69
+ <Input placeholder='email@example.com' type='email' {...field} />
70
+ </FormControl>
71
+ <FormDescription>
72
+ You can manage verified email addresses in your email settings.
73
+ </FormDescription>
74
+ <FormMessage />
75
+ </FormItem>
76
+ )}
77
+ />
78
+
79
+ <FormField
80
+ control={form.control}
81
+ name='bio'
82
+ render={({ field }) => (
83
+ <FormItem>
84
+ <FormLabel>Bio</FormLabel>
85
+ <FormControl>
86
+ <Textarea placeholder='Tell us a little bit about yourself' className='resize-none' {...field} />
87
+ </FormControl>
88
+ <FormDescription>
89
+ You can <span className='font-medium'>@mention</span> other users and organizations.
90
+ </FormDescription>
91
+ <FormMessage />
92
+ </FormItem>
93
+ )}
94
+ />
95
+
96
+ <div className='space-y-2'>
97
+ {fields.map((field, index) => (
98
+ <FormField
99
+ key={field.id}
100
+ control={form.control}
101
+ name={`urls.${index}.value`}
102
+ render={({ field }) => (
103
+ <FormItem>
104
+ {index === 0 && (
105
+ <>
106
+ <FormLabel>URLs</FormLabel>
107
+ <FormDescription>
108
+ Add links to your website, blog, or social media profiles.
109
+ </FormDescription>
110
+ </>
111
+ )}
112
+ <div className='flex gap-2'>
113
+ <FormControl>
114
+ <Input placeholder='https://example.com' {...field} />
115
+ </FormControl>
116
+ {fields.length > 1 && (
117
+ <Button type='button' variant='outline' size='sm' onClick={() => remove(index)}>
118
+ Remove
119
+ </Button>
120
+ )}
121
+ </div>
122
+ <FormMessage />
123
+ </FormItem>
124
+ )}
125
+ />
126
+ ))}
127
+
128
+ {fields.length < 5 && (
129
+ <Button type='button' variant='outline' size='sm' onClick={() => append({ value: '' })}>
130
+ Add URL
131
+ </Button>
132
+ )}
133
+ </div>
134
+
135
+ <Button type='submit'>Update profile</Button>
136
+ </form>
137
+ </Form>
138
+ )
139
+ }
@@ -1,47 +1,8 @@
1
- import { createFileRoute } from '@tanstack/react-router'
2
-
3
- import { PageLayout } from '@/components/layout/page-layout'
4
- import { useCurrentUser } from '@/features/auth/hooks/use-auth'
5
- import { Avatar, AvatarFallback } from '@/components/ui/avatar'
1
+ import { createFileRoute, redirect } from '@tanstack/react-router'
6
2
 
7
3
  export const Route = createFileRoute('/_authenticated/profile')({
8
- component: ProfilePage,
4
+ beforeLoad: () => {
5
+ throw redirect({ to: '/settings' })
6
+ },
7
+ component: () => null,
9
8
  })
10
-
11
- function ProfilePage() {
12
- const { data: user } = useCurrentUser()
13
-
14
- return (
15
- <PageLayout title='Profile' description='Manage your personal information'>
16
- <div className='max-w-lg rounded-lg border bg-card p-6'>
17
- <div className='flex items-center gap-4'>
18
- <Avatar className='h-16 w-16'>
19
- <AvatarFallback className='text-xl'>
20
- {user?.name?.charAt(0)?.toUpperCase() ?? '?'}
21
- </AvatarFallback>
22
- </Avatar>
23
- <div>
24
- <p className='font-semibold'>{user?.name ?? '—'}</p>
25
- <p className='text-sm text-muted-foreground'>{user?.email ?? '—'}</p>
26
- </div>
27
- </div>
28
-
29
- {user?.roles && user.roles.length > 0 && (
30
- <div className='mt-4'>
31
- <p className='text-sm font-medium'>Roles</p>
32
- <div className='mt-1 flex flex-wrap gap-2'>
33
- {user.roles.map((role) => (
34
- <span
35
- key={role}
36
- className='rounded-full bg-secondary px-3 py-1 text-xs font-medium'
37
- >
38
- {role}
39
- </span>
40
- ))}
41
- </div>
42
- </div>
43
- )}
44
- </div>
45
- </PageLayout>
46
- )
47
- }
@@ -0,0 +1,19 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ import { SettingsContentSection } from '@/features/settings/components/settings-content-section'
4
+ import { AccountSettingsForm } from '@/features/settings/account/account-settings-form'
5
+
6
+ export const Route = createFileRoute('/_authenticated/settings/account')({
7
+ component: SettingsAccountPage,
8
+ })
9
+
10
+ function SettingsAccountPage() {
11
+ return (
12
+ <SettingsContentSection
13
+ title='Account'
14
+ desc='Update your account settings. Set your preferred language and timezone.'
15
+ >
16
+ <AccountSettingsForm />
17
+ </SettingsContentSection>
18
+ )
19
+ }
@@ -0,0 +1,19 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ import { SettingsContentSection } from '@/features/settings/components/settings-content-section'
4
+ import { AppearanceSettingsForm } from '@/features/settings/appearance/appearance-settings-form'
5
+
6
+ export const Route = createFileRoute('/_authenticated/settings/appearance')({
7
+ component: SettingsAppearancePage,
8
+ })
9
+
10
+ function SettingsAppearancePage() {
11
+ return (
12
+ <SettingsContentSection
13
+ title='Appearance'
14
+ desc='Customize the appearance of the app. Automatically switch between day and night themes.'
15
+ >
16
+ <AppearanceSettingsForm />
17
+ </SettingsContentSection>
18
+ )
19
+ }
@@ -0,0 +1,18 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ import { SettingsContentSection } from '@/features/settings/components/settings-content-section'
4
+
5
+ export const Route = createFileRoute('/_authenticated/settings/display')({
6
+ component: SettingsDisplayPage,
7
+ })
8
+
9
+ function SettingsDisplayPage() {
10
+ return (
11
+ <SettingsContentSection
12
+ title='Display'
13
+ desc='Turn items on or off to control what is displayed in the app.'
14
+ >
15
+ <p className='text-sm text-muted-foreground'>Display settings coming soon.</p>
16
+ </SettingsContentSection>
17
+ )
18
+ }
@@ -0,0 +1,19 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ import { SettingsContentSection } from '@/features/settings/components/settings-content-section'
4
+ import { ProfileSettingsForm } from '@/features/settings/profile/profile-settings-form'
5
+
6
+ export const Route = createFileRoute('/_authenticated/settings/')({
7
+ component: SettingsProfilePage,
8
+ })
9
+
10
+ function SettingsProfilePage() {
11
+ return (
12
+ <SettingsContentSection
13
+ title='Profile'
14
+ desc='This is how others will see you on the site.'
15
+ >
16
+ <ProfileSettingsForm />
17
+ </SettingsContentSection>
18
+ )
19
+ }
@@ -0,0 +1,18 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ import { SettingsContentSection } from '@/features/settings/components/settings-content-section'
4
+
5
+ export const Route = createFileRoute('/_authenticated/settings/notifications')({
6
+ component: SettingsNotificationsPage,
7
+ })
8
+
9
+ function SettingsNotificationsPage() {
10
+ return (
11
+ <SettingsContentSection
12
+ title='Notifications'
13
+ desc='Configure how you receive notifications.'
14
+ >
15
+ <p className='text-sm text-muted-foreground'>Notification settings coming soon.</p>
16
+ </SettingsContentSection>
17
+ )
18
+ }
@@ -1,16 +1,31 @@
1
- import { createFileRoute } from '@tanstack/react-router'
1
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
2
+ import { Bell, Monitor, Palette, UserCog, Wrench } from 'lucide-react'
2
3
 
3
4
  import { PageLayout } from '@/components/layout/page-layout'
5
+ import { SettingsSidebarNav } from '@/features/settings/components/settings-sidebar-nav'
4
6
 
5
7
  export const Route = createFileRoute('/_authenticated/settings')({
6
- component: SettingsPage,
8
+ component: SettingsLayout,
7
9
  })
8
10
 
9
- function SettingsPage() {
11
+ const sidebarNavItems = [
12
+ { title: 'Profile', href: '/settings', icon: <UserCog size={16} /> },
13
+ { title: 'Account', href: '/settings/account', icon: <Wrench size={16} /> },
14
+ { title: 'Appearance', href: '/settings/appearance', icon: <Palette size={16} /> },
15
+ { title: 'Notifications', href: '/settings/notifications', icon: <Bell size={16} /> },
16
+ { title: 'Display', href: '/settings/display', icon: <Monitor size={16} /> },
17
+ ]
18
+
19
+ function SettingsLayout() {
10
20
  return (
11
- <PageLayout title='Settings' description='Manage your application settings'>
12
- <div className='rounded-lg border bg-card p-6'>
13
- <p className='text-sm text-muted-foreground'>Settings content coming soon.</p>
21
+ <PageLayout title='Settings' description='Manage your account settings and preferences.'>
22
+ <div className='flex flex-col gap-6 lg:flex-row lg:gap-12'>
23
+ <aside className='lg:w-48 shrink-0'>
24
+ <SettingsSidebarNav items={sidebarNavItems} />
25
+ </aside>
26
+ <div className='flex-1 min-w-0'>
27
+ <Outlet />
28
+ </div>
14
29
  </div>
15
30
  </PageLayout>
16
31
  )