added Profile Page and Change password (also included breadcrumb
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ count.txt
|
||||
.output
|
||||
.vinxi
|
||||
todos.json
|
||||
files
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -12,7 +12,7 @@
|
||||
"*.css": "tailwindcss"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
// "source.organizeImports": "always",
|
||||
"source.organizeImports": "always",
|
||||
"source.fixAll": "always"
|
||||
},
|
||||
"editor.suggest.preview": true,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "base-mira",
|
||||
"style": "radix-mira",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles.css",
|
||||
"baseColor": "gray",
|
||||
"baseColor": "stone",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "hugeicons",
|
||||
"iconLibrary": "phosphor",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
|
||||
11966
package-lock.json
generated
11966
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,7 @@
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.0.0",
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@hugeicons/core-free-icons": "^2.0.0",
|
||||
"@hugeicons/react": "^1.1.1",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@prisma/adapter-pg": "^7.1.0",
|
||||
"@prisma/client": "^7.1.0",
|
||||
"@tailwindcss/vite": "^4.0.6",
|
||||
@@ -41,6 +40,7 @@
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"prisma": "^7.1.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.5.0",
|
||||
|
||||
9638
pnpm-lock.yaml
generated
Normal file
9638
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,9 @@
|
||||
|
||||
/** @type {import('prettier').Config} */
|
||||
const config = {
|
||||
semi: false,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
};
|
||||
trailingComma: 'all',
|
||||
}
|
||||
|
||||
export default config;
|
||||
|
||||
45
src/components/AvatarUser.tsx
Normal file
45
src/components/AvatarUser.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Session } from '@/lib/auth/session';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
|
||||
interface AvatarUserProps {
|
||||
session: Session | null | undefined;
|
||||
className?: string;
|
||||
textSize?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
|
||||
}
|
||||
|
||||
const AvatarUser = ({
|
||||
session,
|
||||
className,
|
||||
textSize = 'md',
|
||||
}: AvatarUserProps) => {
|
||||
const imagePath = session?.user?.image
|
||||
? `./files/${session.user.image}`
|
||||
: undefined;
|
||||
const shortName = session?.user?.name
|
||||
?.split(' ')
|
||||
.slice(0, 2)
|
||||
.map((name) => name[0])
|
||||
.join('');
|
||||
|
||||
return (
|
||||
<Avatar className={className}>
|
||||
<AvatarImage src={imagePath} />
|
||||
<AvatarFallback
|
||||
className={cn(
|
||||
'bg-orange-400 text-white',
|
||||
textSize === 'sm' && 'text-xs',
|
||||
textSize === 'md' && 'text-sm',
|
||||
textSize === 'lg' && 'text-xl',
|
||||
textSize === 'xl' && 'text-2xl',
|
||||
textSize === '2xl' && 'text-3xl',
|
||||
textSize === '3xl' && 'text-4xl',
|
||||
)}
|
||||
>
|
||||
{shortName}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarUser;
|
||||
@@ -1,15 +1,10 @@
|
||||
// import { HugeiconsIcon } from '@hugeicons/react'
|
||||
// import { Link } from '@tanstack/react-router'
|
||||
// import {
|
||||
// ArrowDown01Icon,
|
||||
// ArrowRight01Icon,
|
||||
// Home07Icon,
|
||||
// Menu01Icon,
|
||||
// MultiplicationSignIcon,
|
||||
// StickyNote03Icon,
|
||||
// } from '@hugeicons/core-free-icons'
|
||||
// import { useState } from 'react'
|
||||
import { HugeiconsIcon } from '@hugeicons/react'
|
||||
import { Separator } from '@base-ui/react/separator'
|
||||
import { BellIcon } from '@phosphor-icons/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from './auth/auth-provider'
|
||||
import RouterBreadcrumb from './sidebar/RouterBreadcrumb'
|
||||
import { Badge } from './ui/badge'
|
||||
import { Button } from './ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -20,12 +15,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from './ui/dropdown-menu'
|
||||
import { SidebarTrigger } from './ui/sidebar'
|
||||
import { Notification02Icon } from '@hugeicons/core-free-icons'
|
||||
import { Button } from './ui/button'
|
||||
import { Badge } from './ui/badge'
|
||||
import { Separator } from '@base-ui/react/separator'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from './auth/auth-provider'
|
||||
|
||||
export default function Header() {
|
||||
const { t } = useTranslation()
|
||||
@@ -38,35 +27,32 @@ export default function Header() {
|
||||
<SidebarTrigger size="lg" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 data-[orientation=vertical]:h-4"
|
||||
className="mx-2 data-[orientation=vertical]:h-4 border"
|
||||
/>
|
||||
<RouterBreadcrumb />
|
||||
</div>
|
||||
<div className="flex mr-2">
|
||||
{session?.user && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button size="lg" variant="ghost" className="relative" />
|
||||
}
|
||||
>
|
||||
<HugeiconsIcon icon={Notification02Icon} />
|
||||
{false && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs"
|
||||
>
|
||||
0
|
||||
</Badge>
|
||||
)}
|
||||
<span className="sr-only">Notifications</span>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="lg" variant="ghost" className="relative">
|
||||
<BellIcon size={32} />
|
||||
{false && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs"
|
||||
>
|
||||
0
|
||||
</Badge>
|
||||
)}
|
||||
<span className="sr-only">Notifications</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-sm min-w-56 rounded-lg">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="font-bold text-black">
|
||||
{t('ui.label_notifications')}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="font-bold text-black">
|
||||
{t('ui.label_notifications')}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -97,7 +83,6 @@ export default function Header() {
|
||||
'flex-1 flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
|
||||
}}
|
||||
>
|
||||
<HugeiconsIcon icon={StickyNote03Icon} size={20} />
|
||||
<span className="font-medium">Start - SSR Demos</span>
|
||||
</Link> */}
|
||||
</>
|
||||
|
||||
10
src/components/NotFound.tsx
Normal file
10
src/components/NotFound.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-screen">
|
||||
<h1 className="text-4xl font-bold">404</h1>
|
||||
<p className="text-2xl">Page not found</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFound
|
||||
@@ -19,5 +19,6 @@ export function useAuth() {
|
||||
if (!ctx) {
|
||||
throw new Error('useAuth must be used within an AuthProvider')
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
185
src/components/form/change-password-form.tsx
Normal file
185
src/components/form/change-password-form.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { authClient } from '@/lib/auth-client'
|
||||
import i18n from '@/lib/i18n'
|
||||
import { KeyIcon } from '@phosphor-icons/react'
|
||||
import { useForm } from '@tanstack/react-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import z from 'zod'
|
||||
import { Button } from '../ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field'
|
||||
import { Input } from '../ui/input'
|
||||
|
||||
const ChangePasswordFormSchema = z
|
||||
.object({
|
||||
currentPassword: z.string().nonempty(
|
||||
i18n.t('changePassword.messages.is_required', {
|
||||
field: i18n.t('changePassword.form.current_password'),
|
||||
}),
|
||||
),
|
||||
newPassword: z.string().nonempty(
|
||||
i18n.t('changePassword.messages.is_required', {
|
||||
field: i18n.t('changePassword.form.new_password'),
|
||||
}),
|
||||
),
|
||||
confirmPassword: z.string().nonempty(
|
||||
i18n.t('changePassword.messages.is_required', {
|
||||
field: i18n.t('changePassword.form.confirm_password'),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.newPassword !== data.confirmPassword) {
|
||||
ctx.addIssue({
|
||||
path: ['confirmPassword'],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: i18n.t('changePassword.messages.password_not_match'),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
type ChangePassword = z.infer<typeof ChangePasswordFormSchema>
|
||||
|
||||
const defaultValues: ChangePassword = {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
}
|
||||
|
||||
const ChangePasswordForm = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const form = useForm({
|
||||
defaultValues,
|
||||
validators: {
|
||||
onSubmit: ChangePasswordFormSchema,
|
||||
onChange: ChangePasswordFormSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
await authClient.changePassword(
|
||||
{
|
||||
newPassword: value.newPassword,
|
||||
currentPassword: value.currentPassword,
|
||||
revokeOtherSessions: true,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
form.reset()
|
||||
toast.success(t('changePassword.messages.change_password_success'))
|
||||
},
|
||||
onError: (ctx) => {
|
||||
console.log(ctx.error.code)
|
||||
toast.error(t(`backend.${ctx.error.code}` as any))
|
||||
},
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<KeyIcon size={20} />
|
||||
{t('changePassword.ui.title')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
id="change-password-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="currentPassword"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{t('changePassword.form.current_password')}:
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
type="password"
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="newPassword"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{t('changePassword.form.new_password')}:
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
type="password"
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="confirmPassword"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{t('changePassword.form.confirm_password')}:
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
type="password"
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Field>
|
||||
<Button type="submit">{t('ui.change_password_btn')}</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangePasswordForm
|
||||
194
src/components/form/profile-form.tsx
Normal file
194
src/components/form/profile-form.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
import i18n from '@/lib/i18n';
|
||||
import { uploadProfileImage } from '@/server/profile-service';
|
||||
import { UserCircleIcon } from '@phosphor-icons/react';
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import z from 'zod';
|
||||
import { useAuth } from '../auth/auth-provider';
|
||||
import AvatarUser from '../AvatarUser';
|
||||
import { Button } from '../ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
||||
import { Input } from '../ui/input';
|
||||
|
||||
const profileSchema = z.object({
|
||||
name: z.string().nonempty(
|
||||
i18n.t('profile.messages.is_required', {
|
||||
field: i18n.t('profile.form.name'),
|
||||
}),
|
||||
),
|
||||
image: z.instanceof(File).optional(),
|
||||
});
|
||||
|
||||
type Profile = z.infer<typeof profileSchema>;
|
||||
|
||||
const defaultValues: Profile = {
|
||||
name: '',
|
||||
image: undefined,
|
||||
};
|
||||
|
||||
const ProfileForm = () => {
|
||||
const { t } = useTranslation();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { data: session, isLoading } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
...defaultValues,
|
||||
name: session?.user?.name || '',
|
||||
},
|
||||
validators: {
|
||||
onSubmit: profileSchema,
|
||||
onChange: profileSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
try {
|
||||
let imageKey;
|
||||
if (value.image) {
|
||||
// upload image
|
||||
const formData = new FormData();
|
||||
formData.set('file', value.image);
|
||||
const { imageKey: uploadedKey } = await uploadProfileImage({
|
||||
data: formData,
|
||||
});
|
||||
imageKey = uploadedKey;
|
||||
}
|
||||
|
||||
await authClient.updateUser(
|
||||
{
|
||||
name: value.name,
|
||||
image: imageKey,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['session'] });
|
||||
toast.success(t('profile.messages.update_success'));
|
||||
},
|
||||
onError: (ctx) => {
|
||||
toast.error(t(`backend.${ctx.error.code}` as any));
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {}
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) return null;
|
||||
if (!session?.user?.name) return null;
|
||||
|
||||
return (
|
||||
<Card className="@container/card col-span-1 @xl/main:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<UserCircleIcon size={20} />
|
||||
{t('profile.ui.title')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
id="profile-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<FieldGroup>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<AvatarUser
|
||||
session={session}
|
||||
className="h-20 w-20"
|
||||
textSize="2xl"
|
||||
/>
|
||||
<form.Field
|
||||
name="image"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
return (
|
||||
<Field data-invalid={isInvalid} className="col-span-2">
|
||||
<FieldLabel htmlFor={field.name}>Avatar</FieldLabel>
|
||||
<Input
|
||||
type="file"
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
accept=".jpg, .jpeg, .png, .webp"
|
||||
ref={fileInputRef}
|
||||
onChange={(e) =>
|
||||
field.handleChange(e.target.files?.[0])
|
||||
}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<form.Field
|
||||
name="name"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{t('profile.form.name')}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="name">{t('profile.form.email')}</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
value={session?.user?.email || ''}
|
||||
disabled
|
||||
className="disabled:opacity-80"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="name">{t('profile.form.role')}</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
value={session?.user?.role || ''}
|
||||
disabled
|
||||
className="disabled:opacity-80"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">{t('ui.update_btn')}</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileForm;
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Link, useNavigate } from '@tanstack/react-router'
|
||||
import { Button } from './ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card'
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from './ui/field'
|
||||
import { Input } from './ui/input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useForm } from '@tanstack/react-form'
|
||||
import z from 'zod'
|
||||
import i18n from '@/lib/i18n'
|
||||
import { toast } from 'sonner'
|
||||
import { authClient } from '@/lib/auth-client'
|
||||
import i18n from '@/lib/i18n'
|
||||
import { useForm } from '@tanstack/react-form'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { HugeiconsIcon } from '@hugeicons/react'
|
||||
import { AlertCircleIcon, CancelCircleIcon } from '@hugeicons/core-free-icons'
|
||||
import { createLink, useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import z from 'zod'
|
||||
import { Button } from '../ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field'
|
||||
import { Input } from '../ui/input'
|
||||
|
||||
const SignInFormSchema = z.object({
|
||||
email: z
|
||||
@@ -29,6 +27,8 @@ const SignInFormSchema = z.object({
|
||||
),
|
||||
})
|
||||
|
||||
const ButtonLink = createLink(Button)
|
||||
|
||||
const SignInForm = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
@@ -148,9 +148,9 @@ const SignInForm = () => {
|
||||
/>
|
||||
<Field>
|
||||
<Button type="submit">{t('ui.login_btn')}</Button>
|
||||
<Button render={<Link to="/" />} variant="outline">
|
||||
<ButtonLink to="/" variant="outline">
|
||||
{t('ui.cancel_btn')}
|
||||
</Button>
|
||||
</ButtonLink>
|
||||
{/* <FieldDescription className="text-center">
|
||||
{t('loginPage.ui.not_have_account')}{' '}
|
||||
<Link to="/sign-up">{t('ui.signup_btn')}</Link>
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Button } from './ui/button'
|
||||
import { createLink, Link } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '../ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from './ui/card'
|
||||
import { Field, FieldDescription, FieldGroup, FieldLabel } from './ui/field'
|
||||
import { Input } from './ui/input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
} from '../ui/card'
|
||||
import { Field, FieldDescription, FieldGroup, FieldLabel } from '../ui/field'
|
||||
import { Input } from '../ui/input'
|
||||
|
||||
const ButtonLink = createLink(Button)
|
||||
|
||||
const SignupForm = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -56,9 +58,9 @@ const SignupForm = () => {
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Create Account</Button>
|
||||
<Button render={<Link to="/" />} variant="outline">
|
||||
<ButtonLink to="/" variant="outline">
|
||||
{t('ui.cancel_btn')}
|
||||
</Button>
|
||||
</ButtonLink>
|
||||
<FieldDescription className="text-center">
|
||||
Already have an account?{' '}
|
||||
<Link to="/sign-in">{t('ui.login_btn')}</Link>
|
||||
65
src/components/sidebar/RouterBreadcrumb.tsx
Normal file
65
src/components/sidebar/RouterBreadcrumb.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { AnyRouteMatch, Link, useMatches } from '@tanstack/react-router'
|
||||
import { Fragment } from 'react/jsx-runtime'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from '../ui/breadcrumb'
|
||||
|
||||
export type BreadcrumbValue =
|
||||
| string
|
||||
| string[]
|
||||
| ((match: AnyRouteMatch) => string | string[])
|
||||
|
||||
const RouterBreadcrumb = () => {
|
||||
const matches = useMatches()
|
||||
|
||||
const breadcrumbs = matches.flatMap((match) => {
|
||||
const staticData = match.staticData
|
||||
if (!staticData?.breadcrumb) return []
|
||||
|
||||
const breadcrumbValue =
|
||||
typeof staticData.breadcrumb === 'function'
|
||||
? staticData.breadcrumb(match)
|
||||
: staticData.breadcrumb
|
||||
|
||||
const items = Array.isArray(breadcrumbValue)
|
||||
? breadcrumbValue
|
||||
: [breadcrumbValue]
|
||||
|
||||
return items.map((item) => ({
|
||||
label: item,
|
||||
path: match.pathname,
|
||||
}))
|
||||
})
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{breadcrumbs.map((crumb, index) => {
|
||||
const isLast = index === breadcrumbs.length - 1
|
||||
|
||||
return (
|
||||
<Fragment key={`${crumb.path}-${index}`}>
|
||||
<BreadcrumbItem>
|
||||
{isLast ? (
|
||||
<BreadcrumbPage>{crumb.label}</BreadcrumbPage>
|
||||
) : (
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to={crumb.path}>{crumb.label}</Link>
|
||||
</BreadcrumbLink>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
{!isLast && <BreadcrumbSeparator />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
|
||||
export default RouterBreadcrumb
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
SidebarMenuItem,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import NavUser from './nav-user'
|
||||
import NavMain from './nav-main'
|
||||
import NavUser from './nav-user'
|
||||
|
||||
const AppSidebar = ({ ...props }: React.ComponentProps<typeof Sidebar>) => {
|
||||
return (
|
||||
@@ -28,7 +28,6 @@ const AppSidebar = ({ ...props }: React.ComponentProps<typeof Sidebar>) => {
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<NavMain />
|
||||
</SidebarContent>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { GaugeIcon, HouseIcon } from '@phosphor-icons/react'
|
||||
import { createLink } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from '../ui/sidebar'
|
||||
import { HugeiconsIcon } from '@hugeicons/react'
|
||||
import { Home07Icon } from '@hugeicons/core-free-icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const SidebarMenuButtonLink = createLink(SidebarMenuButton)
|
||||
|
||||
const NavMain = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -15,14 +16,22 @@ const NavMain = () => {
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton render={<Link to="/" />}>
|
||||
<HugeiconsIcon icon={Home07Icon} />
|
||||
<SidebarMenuButtonLink
|
||||
to="/"
|
||||
className="cursor-pointer"
|
||||
tooltip={t('nav.home')}
|
||||
>
|
||||
<HouseIcon size={24} />
|
||||
{t('nav.home')}
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuButton render={<Link to="/dashboard" />}>
|
||||
<HugeiconsIcon icon={Home07Icon} />
|
||||
</SidebarMenuButtonLink>
|
||||
<SidebarMenuButtonLink
|
||||
to="/dashboard"
|
||||
className="cursor-pointer"
|
||||
tooltip={t('nav.dashboard')}
|
||||
>
|
||||
<GaugeIcon size={24} />
|
||||
{t('nav.dashboard')}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuButtonLink>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { HugeiconsIcon } from '@hugeicons/react'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
import {
|
||||
DotsThreeVerticalIcon,
|
||||
KeyIcon,
|
||||
SignInIcon,
|
||||
SignOutIcon,
|
||||
UserCircleIcon,
|
||||
} from '@phosphor-icons/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { createLink, useNavigate } from '@tanstack/react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { useAuth } from '../auth/auth-provider';
|
||||
import AvatarUser from '../AvatarUser';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -8,88 +21,75 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '../ui/dropdown-menu'
|
||||
} from '../ui/dropdown-menu';
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '../ui/sidebar'
|
||||
import {
|
||||
LoginSquare01Icon,
|
||||
Logout01FreeIcons,
|
||||
MoreVerticalIcon,
|
||||
UserAccountIcon,
|
||||
} from '@hugeicons/core-free-icons'
|
||||
import { toast } from 'sonner'
|
||||
import { Link, useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '../auth/auth-provider'
|
||||
import { authClient } from '@/lib/auth-client'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
} from '../ui/sidebar';
|
||||
|
||||
const SidebarMenuButtonLink = createLink(SidebarMenuButton);
|
||||
const DropdownMenuItemLink = createLink(DropdownMenuItem);
|
||||
|
||||
const NavUser = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { isMobile } = useSidebar()
|
||||
const queryClient = useQueryClient()
|
||||
const { data: session, isLoading } = useAuth()
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { isMobile } = useSidebar();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: session, isLoading } = useAuth();
|
||||
|
||||
const signout = async () => {
|
||||
await authClient.signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
navigate({ to: '/' })
|
||||
queryClient.invalidateQueries({ queryKey: ['session'] })
|
||||
toast.success(t('loginPage.messages.logout_success'))
|
||||
navigate({ to: '/' });
|
||||
queryClient.invalidateQueries({ queryKey: ['session'] });
|
||||
toast.success(t('loginPage.messages.logout_success'));
|
||||
},
|
||||
onError: (ctx) => {
|
||||
toast.error(t(`backend.${ctx.error.code}` as any))
|
||||
toast.error(t(`backend.${ctx.error.code}` as any));
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) return null
|
||||
if (isLoading) return null;
|
||||
if (!session?.user)
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton tooltip="Sign In">
|
||||
<Link to="/sign-in" className="flex items-center gap-2 w-full">
|
||||
<HugeiconsIcon icon={LoginSquare01Icon} />
|
||||
{t('ui.login_btn')}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuButtonLink
|
||||
to="/sign-in"
|
||||
className="flex items-center gap-2 w-full"
|
||||
tooltip="Sign In"
|
||||
>
|
||||
<SignInIcon size={28} />
|
||||
{t('ui.login_btn')}
|
||||
</SidebarMenuButtonLink>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={session?.user?.image ?? undefined} />
|
||||
<AvatarFallback className="bg-cyan-400 text-white">
|
||||
S
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">
|
||||
{session?.user?.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">{session?.user?.email}</span>
|
||||
</div>
|
||||
<HugeiconsIcon icon={MoreVerticalIcon} />
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground cursor-pointer"
|
||||
tooltip={session?.user?.name}
|
||||
>
|
||||
<AvatarUser session={session} className="h-8 w-8" />
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">
|
||||
{session?.user?.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">{session?.user?.email}</span>
|
||||
</div>
|
||||
<DotsThreeVerticalIcon size={28} />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
@@ -98,37 +98,37 @@ const NavUser = () => {
|
||||
sideOffset={4}
|
||||
>
|
||||
{/* Dropdown menu content */}
|
||||
<DropdownMenuGroup>
|
||||
<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-9 w-9">
|
||||
<AvatarImage src={session?.user?.image ?? undefined} />
|
||||
<AvatarFallback className="bg-cyan-400 text-white">
|
||||
S
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">
|
||||
{session?.user?.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
{session?.user?.email}
|
||||
</span>
|
||||
</div>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<AvatarUser session={session} className="h-8 w-8" />
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">
|
||||
{session?.user?.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
{session?.user?.email}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<HugeiconsIcon icon={UserAccountIcon} />
|
||||
<DropdownMenuItemLink to="/profile" className="cursor-pointer">
|
||||
<UserCircleIcon size={28} />
|
||||
{t('nav.account')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuItemLink>
|
||||
<DropdownMenuItemLink
|
||||
to="/change-password"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<KeyIcon size={28} />
|
||||
{t('nav.change_password')}
|
||||
</DropdownMenuItemLink>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onClick={signout}>
|
||||
<HugeiconsIcon icon={Logout01FreeIcons} />
|
||||
<DropdownMenuItem onClick={signout} className="cursor-pointer">
|
||||
<SignOutIcon size={28} />
|
||||
{t('ui.logout_btn')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
@@ -136,7 +136,7 @@ const NavUser = () => {
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default NavUser
|
||||
export default NavUser;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react"
|
||||
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
|
||||
import { Avatar as AvatarPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -7,7 +7,7 @@ function Avatar({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: AvatarPrimitive.Root.Props & {
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
||||
size?: "default" | "sm" | "lg"
|
||||
}) {
|
||||
return (
|
||||
@@ -23,7 +23,10 @@ function Avatar({
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
@@ -39,7 +42,7 @@ function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: AvatarPrimitive.Fallback.Props) {
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mergeProps } from "@base-ui/react/merge-props"
|
||||
import { useRender } from "@base-ui/react/use-render"
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -26,23 +26,20 @@ const badgeVariants = cva(
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
render,
|
||||
asChild = false,
|
||||
...props
|
||||
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
||||
return useRender({
|
||||
defaultTagName: "span",
|
||||
props: mergeProps<"span">(
|
||||
{
|
||||
className: cn(badgeVariants({ className, variant })),
|
||||
},
|
||||
props
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "badge",
|
||||
variant,
|
||||
},
|
||||
})
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import * as React from "react"
|
||||
import { mergeProps } from "@base-ui/react/merge-props"
|
||||
import { useRender } from "@base-ui/react/use-render"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { ArrowRight01Icon, MoreHorizontalCircle01Icon } from "@hugeicons/core-free-icons"
|
||||
import { CaretRightIcon, DotsThreeIcon } from "@phosphor-icons/react"
|
||||
|
||||
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
@@ -41,23 +39,21 @@ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
render,
|
||||
...props
|
||||
}: useRender.ComponentProps<"a">) {
|
||||
return useRender({
|
||||
defaultTagName: "a",
|
||||
props: mergeProps<"a">(
|
||||
{
|
||||
className: cn("hover:text-foreground transition-colors", className),
|
||||
},
|
||||
props
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "breadcrumb-link",
|
||||
},
|
||||
})
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
@@ -87,7 +83,8 @@ function BreadcrumbSeparator({
|
||||
{...props}
|
||||
>
|
||||
{children ?? (
|
||||
<HugeiconsIcon icon={ArrowRight01Icon} strokeWidth={2} />
|
||||
<CaretRightIcon
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
@@ -108,7 +105,8 @@ function BreadcrumbEllipsis({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<HugeiconsIcon icon={MoreHorizontalCircle01Icon} strokeWidth={2} />
|
||||
<DotsThreeIcon
|
||||
/>
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button as ButtonPrimitive } from '@base-ui/react/button'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { Slot } from 'radix-ui'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -10,7 +11,7 @@ const buttonVariants = cva(
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
outline:
|
||||
'border-border dark:bg-input/20 dark:bg-input/30 hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground',
|
||||
'border-border dark:bg-input/30 hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
||||
ghost:
|
||||
@@ -42,11 +43,19 @@ function Button({
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
asChild = false,
|
||||
...props
|
||||
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : 'button'
|
||||
|
||||
return (
|
||||
<ButtonPrimitive
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,191 +1,119 @@
|
||||
import * as React from 'react'
|
||||
import { Menu as MenuPrimitive } from '@base-ui/react/menu'
|
||||
import * as React from "react"
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { HugeiconsIcon } from '@hugeicons/react'
|
||||
import { ArrowRight01Icon, Tick02Icon } from '@hugeicons/core-free-icons'
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckIcon, CaretRightIcon } from "@phosphor-icons/react"
|
||||
|
||||
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
|
||||
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
|
||||
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
|
||||
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
align = 'start',
|
||||
alignOffset = 0,
|
||||
side = 'bottom',
|
||||
sideOffset = 4,
|
||||
className,
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: MenuPrimitive.Popup.Props &
|
||||
Pick<
|
||||
MenuPrimitive.Positioner.Props,
|
||||
'align' | 'alignOffset' | 'side' | 'sideOffset'
|
||||
>) {
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<MenuPrimitive.Portal>
|
||||
<MenuPrimitive.Positioner
|
||||
className="isolate z-50 outline-none"
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
>
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
className={cn(
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenuPrimitive.Positioner>
|
||||
</MenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
|
||||
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: MenuPrimitive.GroupLabel.Props & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<MenuPrimitive.GroupLabel
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
'text-muted-foreground px-2 py-1.5 text-xs data-inset:pl-8',
|
||||
className,
|
||||
)}
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
align = "start",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto data-[state=closed]:overflow-hidden", className )}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = 'default',
|
||||
variant = "default",
|
||||
...props
|
||||
}: MenuPrimitive.Item.Props & {
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: 'default' | 'destructive'
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.Item
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs/relaxed [&_svg:not([class*='size-'])]:size-3.5 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs/relaxed [&_svg:not([class*='size-'])]:size-3.5 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
|
||||
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: MenuPrimitive.SubmenuTrigger.Props & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.SubmenuTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs [&_svg:not([class*='size-'])]:size-3.5 flex cursor-default items-center outline-hidden select-none data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
strokeWidth={2}
|
||||
className="ml-auto"
|
||||
/>
|
||||
</MenuPrimitive.SubmenuTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
align = 'start',
|
||||
alignOffset = -3,
|
||||
side = 'right',
|
||||
sideOffset = 0,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuContent>) {
|
||||
return (
|
||||
<DropdownMenuContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 w-auto',
|
||||
className,
|
||||
)}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: MenuPrimitive.CheckboxItem.Props) {
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<MenuPrimitive.CheckboxItem
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground min-h-7 gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs [&_svg:not([class*='size-'])]:size-3.5 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground min-h-7 gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs [&_svg:not([class*='size-'])]:size-3.5 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
<MenuPrimitive.CheckboxItemIndicator>
|
||||
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} />
|
||||
</MenuPrimitive.CheckboxItemIndicator>
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon
|
||||
/>
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenuPrimitive.CheckboxItem>
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<MenuPrimitive.RadioGroup
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
@@ -196,37 +124,55 @@ function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: MenuPrimitive.RadioItem.Props) {
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<MenuPrimitive.RadioItem
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground min-h-7 gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs [&_svg:not([class*='size-'])]:size-3.5 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground min-h-7 gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs [&_svg:not([class*='size-'])]:size-3.5 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
<MenuPrimitive.RadioItemIndicator>
|
||||
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} />
|
||||
</MenuPrimitive.RadioItemIndicator>
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon
|
||||
/>
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenuPrimitive.RadioItem>
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs data-[inset]:pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: MenuPrimitive.Separator.Props) {
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<MenuPrimitive.Separator
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn('bg-border/50 -mx-1 my-1 h-px', className)}
|
||||
className={cn("bg-border/50 -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -235,15 +181,55 @@ function DropdownMenuSeparator({
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'span'>) {
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-[0.625rem] tracking-widest", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-[0.625rem] tracking-widest',
|
||||
className,
|
||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs [&_svg:not([class*='size-'])]:size-3.5 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<CaretRightIcon className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 z-50 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden", className )}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import * as React from "react"
|
||||
import { Input as InputPrimitive } from '@base-ui/react/input'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<InputPrimitive
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
'bg-input/20 dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-7 rounded-md border px-2 py-0.5 text-sm transition-colors file:h-6 file:text-xs/relaxed file:font-medium focus-visible:ring-2 aria-invalid:ring-2 md:text-xs/relaxed file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'bg-input/20 dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-7 rounded-md border px-2 py-0.5 text-sm transition-colors file:h-6 file:text-xs/relaxed file:font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] md:text-xs/relaxed file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 disabled:border-none disabled:p-0 disabled:bg-transparent disabled:text-black',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import * as React from "react"
|
||||
import { Label as LabelPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<label
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"gap-2 text-xs/relaxed leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { Separator as SeparatorPrimitive } from '@base-ui/react/separator'
|
||||
import * as React from "react"
|
||||
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = 'horizontal',
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: SeparatorPrimitive.Props) {
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px',
|
||||
className,
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
import * as React from "react"
|
||||
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
|
||||
import { Dialog as SheetPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { Cancel01Icon } from "@hugeicons/core-free-icons"
|
||||
import { XIcon } from "@phosphor-icons/react"
|
||||
|
||||
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Backdrop
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/80 duration-100 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)}
|
||||
{...props}
|
||||
@@ -38,14 +46,14 @@ function SheetContent({
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: SheetPrimitive.Popup.Props & {
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Popup
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
data-side={side}
|
||||
className={cn("bg-background data-open:animate-in data-closed:animate-out data-[side=right]:data-closed:slide-out-to-right-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 fixed z-50 flex flex-col bg-clip-padding text-xs/relaxed shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm", className)}
|
||||
@@ -53,21 +61,15 @@ function SheetContent({
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close
|
||||
data-slot="sheet-close"
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-4 right-4"
|
||||
size="icon-sm"
|
||||
<SheetPrimitive.Close data-slot="sheet-close" asChild>
|
||||
<Button variant="ghost" className="absolute top-4 right-4" size="icon-sm">
|
||||
<XIcon
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Popup>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
@@ -92,7 +94,10 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
@@ -105,7 +110,7 @@ function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: SheetPrimitive.Description.Props) {
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
'use client'
|
||||
"use client"
|
||||
|
||||
import * as React from 'react'
|
||||
import { mergeProps } from '@base-ui/react/merge-props'
|
||||
import { useRender } from '@base-ui/react/use-render'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
} from "@/components/ui/sheet"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { useIsMobile } from '@/hooks/use-mobile'
|
||||
import { HugeiconsIcon } from '@hugeicons/react'
|
||||
import { SidebarLeftIcon } from '@hugeicons/core-free-icons'
|
||||
} from "@/components/ui/tooltip"
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { SidebarIcon } from "@phosphor-icons/react"
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = 'sidebar_state'
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = '19rem'
|
||||
const SIDEBAR_WIDTH_MOBILE = '18rem'
|
||||
const SIDEBAR_WIDTH_ICON = '3rem'
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: 'expanded' | 'collapsed'
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
openMobile: boolean
|
||||
@@ -48,7 +46,7 @@ const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
if (!context) {
|
||||
throw new Error('useSidebar must be used within a SidebarProvider.')
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
}
|
||||
|
||||
return context
|
||||
@@ -62,7 +60,7 @@ function SidebarProvider({
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & {
|
||||
}: React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
@@ -76,7 +74,7 @@ function SidebarProvider({
|
||||
const open = openProp ?? _open
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === 'function' ? value(open) : value
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
} else {
|
||||
@@ -86,7 +84,7 @@ function SidebarProvider({
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
},
|
||||
[setOpenProp, open],
|
||||
[setOpenProp, open]
|
||||
)
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
@@ -106,13 +104,13 @@ function SidebarProvider({
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [toggleSidebar])
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? 'expanded' : 'collapsed'
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
@@ -124,7 +122,7 @@ function SidebarProvider({
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -133,14 +131,14 @@ function SidebarProvider({
|
||||
data-slot="sidebar-wrapper"
|
||||
style={
|
||||
{
|
||||
'--sidebar-width': SIDEBAR_WIDTH,
|
||||
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
|
||||
className,
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -151,26 +149,26 @@ function SidebarProvider({
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
side = 'left',
|
||||
variant = 'sidebar',
|
||||
collapsible = 'offExamples',
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offExamples",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & {
|
||||
side?: 'left' | 'right'
|
||||
variant?: 'sidebar' | 'floating' | 'inset'
|
||||
collapsible?: 'offExamples' | 'icon' | 'none'
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right"
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offExamples" | "icon" | "none"
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
|
||||
if (collapsible === 'none') {
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
'bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col',
|
||||
className,
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -189,7 +187,7 @@ function Sidebar({
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
@@ -208,7 +206,7 @@ function Sidebar({
|
||||
<div
|
||||
className="group peer text-sidebar-foreground hidden md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === 'collapsed' ? collapsible : ''}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
data-slot="sidebar"
|
||||
@@ -217,26 +215,26 @@ function Sidebar({
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
'transition-[width] duration-200 ease-linear relative w-(--sidebar-width) bg-transparent',
|
||||
'group-data-[collapsible=offExamples]:w-0',
|
||||
'group-data-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
||||
"transition-[width] duration-200 ease-linear relative w-(--sidebar-width) bg-transparent",
|
||||
"group-data-[collapsible=offExamples]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
|
||||
side === 'left'
|
||||
? 'left-0 group-data-[collapsible=offExamples]:left-[calc(var(--sidebar-width)*-1)]'
|
||||
: 'right-0 group-data-[collapsible=offExamples]:right-[calc(var(--sidebar-width)*-1)]',
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offExamples]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offExamples]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||
className,
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -272,13 +270,14 @@ function SidebarTrigger({
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<HugeiconsIcon icon={SidebarLeftIcon} strokeWidth={2} />
|
||||
<SidebarIcon
|
||||
/>
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
@@ -290,26 +289,26 @@ function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-0.5 sm:flex',
|
||||
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
|
||||
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||
'hover:group-data-[collapsible=offExamples]:bg-sidebar group-data-[collapsible=offExamples]:translate-x-0 group-data-[collapsible=offExamples]:after:left-full',
|
||||
'[[data-side=left][data-collapsible=offExamples]_&]:-right-2',
|
||||
'[[data-side=right][data-collapsible=offExamples]_&]:-left-2',
|
||||
className,
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"hover:group-data-[collapsible=offExamples]:bg-sidebar group-data-[collapsible=offExamples]:translate-x-0 group-data-[collapsible=offExamples]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offExamples]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offExamples]_&]:-left-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
return (
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
'bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 relative flex w-full flex-1 flex-col',
|
||||
className,
|
||||
"bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 relative flex w-full flex-1 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -324,32 +323,29 @@ function SidebarInput({
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn(
|
||||
'bg-muted/20 dark:bg-muted/30 border-input h-8 w-full',
|
||||
className,
|
||||
)}
|
||||
className={cn("bg-muted/20 dark:bg-muted/30 border-input h-8 w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-header"
|
||||
data-sidebar="header"
|
||||
className={cn('gap-2 p-2 flex flex-col', className)}
|
||||
className={cn("gap-2 p-2 flex flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
className={cn('gap-2 p-2 flex flex-col', className)}
|
||||
className={cn("gap-2 p-2 flex flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -363,34 +359,34 @@ function SidebarSeparator({
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn('bg-sidebar-border mx-2 w-auto', className)}
|
||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
'no-scrollbar gap-0 flex min-h-0 flex-1 flex-col overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
||||
className,
|
||||
"no-scrollbar gap-0 flex min-h-0 flex-1 flex-col overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group"
|
||||
data-sidebar="group"
|
||||
className={cn(
|
||||
'px-2 py-1 relative flex w-full min-w-0 flex-col',
|
||||
className,
|
||||
"px-2 py-1 relative flex w-full min-w-0 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -399,146 +395,133 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
|
||||
function SidebarGroupLabel({
|
||||
className,
|
||||
render,
|
||||
asChild = false,
|
||||
...props
|
||||
}: useRender.ComponentProps<'div'> & React.ComponentProps<'div'>) {
|
||||
return useRender({
|
||||
defaultTagName: 'div',
|
||||
props: mergeProps<'div'>(
|
||||
{
|
||||
className: cn(
|
||||
'text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0',
|
||||
className,
|
||||
),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: 'sidebar-group-label',
|
||||
sidebar: 'group-label',
|
||||
},
|
||||
})
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "div"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
className,
|
||||
render,
|
||||
asChild = false,
|
||||
...props
|
||||
}: useRender.ComponentProps<'button'> & React.ComponentProps<'button'>) {
|
||||
return useRender({
|
||||
defaultTagName: 'button',
|
||||
props: mergeProps<'button'>(
|
||||
{
|
||||
className: cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 w-5 rounded-md p-0 focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: 'sidebar-group-action',
|
||||
sidebar: 'group-action',
|
||||
},
|
||||
})
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 w-5 rounded-md p-0 focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'div'>) {
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
className={cn('text-xs w-full', className)}
|
||||
className={cn("text-xs w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
className={cn('gap-px flex w-full min-w-0 flex-col', className)}
|
||||
className={cn("gap-px flex w-full min-w-0 flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-item"
|
||||
data-sidebar="menu-item"
|
||||
className={cn('group/menu-item relative', className)}
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
'ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-[calc(var(--radius-sm)+2px)] p-2 text-left text-xs transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
"ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-[calc(var(--radius-sm)+2px)] p-2 text-left text-xs transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
outline:
|
||||
'bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline: "bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 text-xs',
|
||||
sm: 'h-7 text-xs',
|
||||
lg: 'h-12 text-xs group-data-[collapsible=icon]:p-0!',
|
||||
default: "h-8 text-xs",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-xs group-data-[collapsible=icon]:p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function SidebarMenuButton({
|
||||
render,
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
}: useRender.ComponentProps<'button'> &
|
||||
React.ComponentProps<'button'> & {
|
||||
isActive?: boolean
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
isActive?: boolean
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
const comp = useRender({
|
||||
defaultTagName: 'button',
|
||||
props: mergeProps<'button'>(
|
||||
{
|
||||
className: cn(sidebarMenuButtonVariants({ variant, size }), className),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render: !tooltip ? render : TooltipTrigger,
|
||||
state: {
|
||||
slot: 'sidebar-menu-button',
|
||||
sidebar: 'menu-button',
|
||||
size,
|
||||
active: isActive,
|
||||
},
|
||||
})
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!tooltip) {
|
||||
return comp
|
||||
return button
|
||||
}
|
||||
|
||||
if (typeof tooltip === 'string') {
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
}
|
||||
@@ -546,11 +529,11 @@ function SidebarMenuButton({
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
{comp}
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== 'collapsed' || isMobile}
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -559,45 +542,41 @@ function SidebarMenuButton({
|
||||
|
||||
function SidebarMenuAction({
|
||||
className,
|
||||
render,
|
||||
asChild = false,
|
||||
showOnHover = false,
|
||||
...props
|
||||
}: useRender.ComponentProps<'button'> &
|
||||
React.ComponentProps<'button'> & {
|
||||
showOnHover?: boolean
|
||||
}) {
|
||||
return useRender({
|
||||
defaultTagName: 'button',
|
||||
props: mergeProps<'button'>(
|
||||
{
|
||||
className: cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 aspect-square w-5 rounded-[calc(var(--radius-sm)-2px)] p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0',
|
||||
showOnHover &&
|
||||
'peer-data-active/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-open:opacity-100 md:opacity-0',
|
||||
className,
|
||||
),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: 'sidebar-menu-action',
|
||||
sidebar: 'menu-action',
|
||||
},
|
||||
})
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
showOnHover?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 aspect-square w-5 rounded-[calc(var(--radius-sm)-2px)] p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0",
|
||||
showOnHover &&
|
||||
"peer-data-active/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-open:opacity-100 md:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'div'>) {
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
'text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 h-5 min-w-5 rounded-[calc(var(--radius-sm)-2px)] px-1 text-xs font-medium peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 flex items-center justify-center tabular-nums select-none group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
"text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 rounded-[calc(var(--radius-sm)-2px)] px-1 text-xs font-medium peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 flex items-center justify-center tabular-nums select-none group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -608,7 +587,7 @@ function SidebarMenuSkeleton({
|
||||
className,
|
||||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & {
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
@@ -620,7 +599,7 @@ function SidebarMenuSkeleton({
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn('h-8 gap-2 rounded-md px-2 flex items-center', className)}
|
||||
className={cn("h-8 gap-2 rounded-md px-2 flex items-center", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
@@ -634,7 +613,7 @@ function SidebarMenuSkeleton({
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
'--skeleton-width': width,
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
@@ -642,15 +621,12 @@ function SidebarMenuSkeleton({
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
'border-sidebar-border mx-3.5 translate-x-px gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden flex min-w-0 flex-col',
|
||||
className,
|
||||
)}
|
||||
className={cn("border-sidebar-border mx-3.5 translate-x-px gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden flex min-w-0 flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -659,47 +635,43 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
|
||||
function SidebarMenuSubItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'li'>) {
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
className={cn('group/menu-sub-item relative', className)}
|
||||
className={cn("group/menu-sub-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSubButton({
|
||||
render,
|
||||
size = 'md',
|
||||
asChild = false,
|
||||
size = "md",
|
||||
isActive = false,
|
||||
className,
|
||||
...props
|
||||
}: useRender.ComponentProps<'a'> &
|
||||
React.ComponentProps<'a'> & {
|
||||
size?: 'sm' | 'md'
|
||||
isActive?: boolean
|
||||
}) {
|
||||
return useRender({
|
||||
defaultTagName: 'a',
|
||||
props: mergeProps<'a'>(
|
||||
{
|
||||
className: cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-xs data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0',
|
||||
className,
|
||||
),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: 'sidebar-menu-sub-button',
|
||||
sidebar: 'menu-sub-button',
|
||||
size,
|
||||
active: isActive,
|
||||
},
|
||||
})
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
size?: "sm" | "md"
|
||||
isActive?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-sub-button"
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-xs data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { CheckmarkCircle02Icon, InformationCircleIcon, Alert02Icon, MultiplicationSignCircleIcon, Loading03Icon } from "@hugeicons/core-free-icons"
|
||||
import { CheckCircleIcon, InfoIcon, WarningIcon, XCircleIcon, SpinnerIcon } from "@phosphor-icons/react"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
@@ -12,19 +11,19 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: (
|
||||
<HugeiconsIcon icon={CheckmarkCircle02Icon} strokeWidth={2} className="size-4" />
|
||||
<CheckCircleIcon className="size-4" />
|
||||
),
|
||||
info: (
|
||||
<HugeiconsIcon icon={InformationCircleIcon} strokeWidth={2} className="size-4" />
|
||||
<InfoIcon className="size-4" />
|
||||
),
|
||||
warning: (
|
||||
<HugeiconsIcon icon={Alert02Icon} strokeWidth={2} className="size-4" />
|
||||
<WarningIcon className="size-4" />
|
||||
),
|
||||
error: (
|
||||
<HugeiconsIcon icon={MultiplicationSignCircleIcon} strokeWidth={2} className="size-4" />
|
||||
<XCircleIcon className="size-4" />
|
||||
),
|
||||
loading: (
|
||||
<HugeiconsIcon icon={Loading03Icon} strokeWidth={2} className="size-4 animate-spin" />
|
||||
<SpinnerIcon className="size-4 animate-spin" />
|
||||
),
|
||||
}}
|
||||
style={
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
||||
import { Tooltip as TooltipPrimitive } from 'radix-ui'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delay = 0,
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: TooltipPrimitive.Provider.Props) {
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delay={delay}
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
@@ -25,46 +28,34 @@ function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
side = "top",
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: TooltipPrimitive.Popup.Props &
|
||||
Pick<
|
||||
TooltipPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset"
|
||||
>) {
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Positioner
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className="isolate z-50"
|
||||
className={cn(
|
||||
'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 rounded-md px-3 py-1.5 text-xs **:data-[slot=kbd]:rounded-md bg-foreground text-background z-50 w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin)',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<TooltipPrimitive.Popup
|
||||
data-slot="tooltip-content"
|
||||
className={cn(
|
||||
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 rounded-md px-3 py-1.5 text-xs **:data-[slot=kbd]:rounded-md bg-foreground text-background z-50 w-fit max-w-xs origin-(--transform-origin)",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground z-50 data-[side=bottom]:top-1 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
||||
</TooltipPrimitive.Popup>
|
||||
</TooltipPrimitive.Positioner>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground z-50" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
import { betterAuth } from 'better-auth'
|
||||
import { admin as adminPlugin, organization } from 'better-auth/plugins'
|
||||
import { prismaAdapter } from 'better-auth/adapters/prisma'
|
||||
import { prisma } from '@/db'
|
||||
import { ac, admin, user } from '@/lib/auth/permissions'
|
||||
import { prisma } from '@/db';
|
||||
import { User } from '@/generated/prisma/client';
|
||||
import { SessionModel } from '@/generated/prisma/models';
|
||||
import {
|
||||
acOrg,
|
||||
adminOrg,
|
||||
member,
|
||||
owner,
|
||||
} from '@/lib/auth/organization-permissions'
|
||||
} from '@/lib/auth/organization-permissions';
|
||||
import { ac, admin, user } from '@/lib/auth/permissions';
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
||||
import { admin as adminPlugin, organization } from 'better-auth/plugins';
|
||||
|
||||
// export interface User {
|
||||
// image?: string | null;
|
||||
// name?: string | null;
|
||||
// email?: string | null;
|
||||
// }
|
||||
|
||||
export interface Session {
|
||||
session: SessionModel | null | undefined;
|
||||
user?: User;
|
||||
}
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: prismaAdapter(prisma, {
|
||||
@@ -52,9 +65,9 @@ export const auth = betterAuth({
|
||||
userId: user.id,
|
||||
color: '#000000',
|
||||
},
|
||||
})
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { createServerFn } from '@tanstack/react-start'
|
||||
import { getRequestHeaders } from '@tanstack/react-start/server'
|
||||
import { auth } from '../auth'
|
||||
import { createServerFn } from '@tanstack/react-start';
|
||||
import { getRequestHeaders } from '@tanstack/react-start/server';
|
||||
import { auth } from '../auth';
|
||||
|
||||
export type Session = typeof auth.$Infer.Session;
|
||||
|
||||
export const getSession = createServerFn({ method: 'GET' }).handler(
|
||||
async () => {
|
||||
const headers = getRequestHeaders()
|
||||
const session = await auth.api.getSession({ headers })
|
||||
return session
|
||||
const headers = getRequestHeaders();
|
||||
const session = await auth.api.getSession({ headers });
|
||||
return session;
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
@@ -11,8 +11,11 @@
|
||||
"confirm_btn": "Xác nhận",
|
||||
"signup_btn": "Đăng ký",
|
||||
"view_btn": "Xem",
|
||||
"save_btn": "Lưu",
|
||||
"update_btn": "Cập nhật",
|
||||
"view_all_notifications": "Xem tất cả thông báo",
|
||||
"label_notifications": "Thông báo"
|
||||
"label_notifications": "Thông báo",
|
||||
"change_password_btn": "Đổi mật khẩu"
|
||||
},
|
||||
"header": {
|
||||
"Administration": "Quản lý",
|
||||
@@ -31,7 +34,8 @@
|
||||
"log": "Lịch sử",
|
||||
"roles": "Vai trò & quyền hạn",
|
||||
"box": "Hộp chứa",
|
||||
"account": "Tài khoản"
|
||||
"account": "Tài khoản",
|
||||
"profile": "Hồ sơ"
|
||||
},
|
||||
"loginPage": {
|
||||
"form": {
|
||||
@@ -50,13 +54,43 @@
|
||||
"email_invalid": "Email không đúng định dạng!"
|
||||
}
|
||||
},
|
||||
"SignUpPage": {
|
||||
"signUpPage": {
|
||||
"form": {},
|
||||
"ui": {
|
||||
"title": "Đăng ký",
|
||||
"create_account": "Tạo tài khoản"
|
||||
},
|
||||
"messages": {}
|
||||
},
|
||||
"changePassword": {
|
||||
"form": {
|
||||
"current_password": "Mật khẩu hiện tại",
|
||||
"new_password": "Mật khẩu mới",
|
||||
"confirm_password": "Nhập lại mật khẩu mới"
|
||||
},
|
||||
"ui": {
|
||||
"title": "Đổi mật khẩu"
|
||||
},
|
||||
"messages": {
|
||||
"is_required": "{{field}} là bắt buộc.",
|
||||
"password_not_match": "Mật khẩu không khớp",
|
||||
"change_password_success": "Đổi mật khẩu thành công!"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"form": {
|
||||
"name": "Tên",
|
||||
"email": "Email",
|
||||
"role": "Vai trò"
|
||||
},
|
||||
"ui": {
|
||||
"title": "Hồ sơ"
|
||||
},
|
||||
"messages": {
|
||||
"is_required": "{{field}} là bắt buộc.",
|
||||
"update_success": "Cập nhật thành công!"
|
||||
}
|
||||
},
|
||||
"kanri": {
|
||||
"settings": "Cài đặt",
|
||||
"settings_desc": "Cài đặt hệ thống.",
|
||||
@@ -232,6 +266,7 @@
|
||||
"page_show": "{count, selectordinal, =1 {Hiện chỉ có # vai trò} other {Hiển thị {start} tới {end} của # hộp}}"
|
||||
},
|
||||
"backend": {
|
||||
"INVALID_EMAIL_OR_PASSWORD": "Email hoặc mật khẩu không đúng!"
|
||||
"INVALID_EMAIL_OR_PASSWORD": "Email hoặc mật khẩu không đúng!",
|
||||
"INVALID_PASSWORD": "Mật khẩu hiện tại không đúng!"
|
||||
}
|
||||
}
|
||||
|
||||
12
src/logo.svg
12
src/logo.svg
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 19 KiB |
@@ -13,8 +13,11 @@ import { Route as appRouteRouteImport } from './routes/(app)/route'
|
||||
import { Route as appIndexRouteImport } from './routes/(app)/index'
|
||||
import { Route as authSignUpRouteImport } from './routes/(auth)/sign-up'
|
||||
import { Route as authSignInRouteImport } from './routes/(auth)/sign-in'
|
||||
import { Route as appDashboardRouteImport } from './routes/(app)/dashboard'
|
||||
import { Route as appauthRouteRouteImport } from './routes/(app)/(auth)/route'
|
||||
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth.$'
|
||||
import { Route as appauthProfileRouteImport } from './routes/(app)/(auth)/profile'
|
||||
import { Route as appauthDashboardRouteImport } from './routes/(app)/(auth)/dashboard'
|
||||
import { Route as appauthChangePasswordRouteImport } from './routes/(app)/(auth)/change-password'
|
||||
|
||||
const appRouteRoute = appRouteRouteImport.update({
|
||||
id: '/(app)',
|
||||
@@ -35,9 +38,8 @@ const authSignInRoute = authSignInRouteImport.update({
|
||||
path: '/sign-in',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const appDashboardRoute = appDashboardRouteImport.update({
|
||||
id: '/dashboard',
|
||||
path: '/dashboard',
|
||||
const appauthRouteRoute = appauthRouteRouteImport.update({
|
||||
id: '/(auth)',
|
||||
getParentRoute: () => appRouteRoute,
|
||||
} as any)
|
||||
const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
|
||||
@@ -45,42 +47,81 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
|
||||
path: '/api/auth/$',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const appauthProfileRoute = appauthProfileRouteImport.update({
|
||||
id: '/profile',
|
||||
path: '/profile',
|
||||
getParentRoute: () => appauthRouteRoute,
|
||||
} as any)
|
||||
const appauthDashboardRoute = appauthDashboardRouteImport.update({
|
||||
id: '/dashboard',
|
||||
path: '/dashboard',
|
||||
getParentRoute: () => appauthRouteRoute,
|
||||
} as any)
|
||||
const appauthChangePasswordRoute = appauthChangePasswordRouteImport.update({
|
||||
id: '/change-password',
|
||||
path: '/change-password',
|
||||
getParentRoute: () => appauthRouteRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/dashboard': typeof appDashboardRoute
|
||||
'/sign-in': typeof authSignInRoute
|
||||
'/sign-up': typeof authSignUpRoute
|
||||
'/': typeof appIndexRoute
|
||||
'/change-password': typeof appauthChangePasswordRoute
|
||||
'/dashboard': typeof appauthDashboardRoute
|
||||
'/profile': typeof appauthProfileRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/dashboard': typeof appDashboardRoute
|
||||
'/sign-in': typeof authSignInRoute
|
||||
'/sign-up': typeof authSignUpRoute
|
||||
'/': typeof appIndexRoute
|
||||
'/change-password': typeof appauthChangePasswordRoute
|
||||
'/dashboard': typeof appauthDashboardRoute
|
||||
'/profile': typeof appauthProfileRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/(app)': typeof appRouteRouteWithChildren
|
||||
'/(app)/dashboard': typeof appDashboardRoute
|
||||
'/(app)/(auth)': typeof appauthRouteRouteWithChildren
|
||||
'/(auth)/sign-in': typeof authSignInRoute
|
||||
'/(auth)/sign-up': typeof authSignUpRoute
|
||||
'/(app)/': typeof appIndexRoute
|
||||
'/(app)/(auth)/change-password': typeof appauthChangePasswordRoute
|
||||
'/(app)/(auth)/dashboard': typeof appauthDashboardRoute
|
||||
'/(app)/(auth)/profile': typeof appauthProfileRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/dashboard' | '/sign-in' | '/sign-up' | '/' | '/api/auth/$'
|
||||
fullPaths:
|
||||
| '/sign-in'
|
||||
| '/sign-up'
|
||||
| '/'
|
||||
| '/change-password'
|
||||
| '/dashboard'
|
||||
| '/profile'
|
||||
| '/api/auth/$'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/dashboard' | '/sign-in' | '/sign-up' | '/' | '/api/auth/$'
|
||||
to:
|
||||
| '/sign-in'
|
||||
| '/sign-up'
|
||||
| '/'
|
||||
| '/change-password'
|
||||
| '/dashboard'
|
||||
| '/profile'
|
||||
| '/api/auth/$'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/(app)'
|
||||
| '/(app)/dashboard'
|
||||
| '/(app)/(auth)'
|
||||
| '/(auth)/sign-in'
|
||||
| '/(auth)/sign-up'
|
||||
| '/(app)/'
|
||||
| '/(app)/(auth)/change-password'
|
||||
| '/(app)/(auth)/dashboard'
|
||||
| '/(app)/(auth)/profile'
|
||||
| '/api/auth/$'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
@@ -121,11 +162,11 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof authSignInRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/(app)/dashboard': {
|
||||
id: '/(app)/dashboard'
|
||||
path: '/dashboard'
|
||||
fullPath: '/dashboard'
|
||||
preLoaderRoute: typeof appDashboardRouteImport
|
||||
'/(app)/(auth)': {
|
||||
id: '/(app)/(auth)'
|
||||
path: ''
|
||||
fullPath: ''
|
||||
preLoaderRoute: typeof appauthRouteRouteImport
|
||||
parentRoute: typeof appRouteRoute
|
||||
}
|
||||
'/api/auth/$': {
|
||||
@@ -135,16 +176,53 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ApiAuthSplatRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/(app)/(auth)/profile': {
|
||||
id: '/(app)/(auth)/profile'
|
||||
path: '/profile'
|
||||
fullPath: '/profile'
|
||||
preLoaderRoute: typeof appauthProfileRouteImport
|
||||
parentRoute: typeof appauthRouteRoute
|
||||
}
|
||||
'/(app)/(auth)/dashboard': {
|
||||
id: '/(app)/(auth)/dashboard'
|
||||
path: '/dashboard'
|
||||
fullPath: '/dashboard'
|
||||
preLoaderRoute: typeof appauthDashboardRouteImport
|
||||
parentRoute: typeof appauthRouteRoute
|
||||
}
|
||||
'/(app)/(auth)/change-password': {
|
||||
id: '/(app)/(auth)/change-password'
|
||||
path: '/change-password'
|
||||
fullPath: '/change-password'
|
||||
preLoaderRoute: typeof appauthChangePasswordRouteImport
|
||||
parentRoute: typeof appauthRouteRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface appauthRouteRouteChildren {
|
||||
appauthChangePasswordRoute: typeof appauthChangePasswordRoute
|
||||
appauthDashboardRoute: typeof appauthDashboardRoute
|
||||
appauthProfileRoute: typeof appauthProfileRoute
|
||||
}
|
||||
|
||||
const appauthRouteRouteChildren: appauthRouteRouteChildren = {
|
||||
appauthChangePasswordRoute: appauthChangePasswordRoute,
|
||||
appauthDashboardRoute: appauthDashboardRoute,
|
||||
appauthProfileRoute: appauthProfileRoute,
|
||||
}
|
||||
|
||||
const appauthRouteRouteWithChildren = appauthRouteRoute._addFileChildren(
|
||||
appauthRouteRouteChildren,
|
||||
)
|
||||
|
||||
interface appRouteRouteChildren {
|
||||
appDashboardRoute: typeof appDashboardRoute
|
||||
appauthRouteRoute: typeof appauthRouteRouteWithChildren
|
||||
appIndexRoute: typeof appIndexRoute
|
||||
}
|
||||
|
||||
const appRouteRouteChildren: appRouteRouteChildren = {
|
||||
appDashboardRoute: appDashboardRoute,
|
||||
appauthRouteRoute: appauthRouteRouteWithChildren,
|
||||
appIndexRoute: appIndexRoute,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,15 @@ import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query
|
||||
import * as TanstackQuery from './integrations/tanstack-query/root-provider'
|
||||
|
||||
// Import the generated route tree
|
||||
import { BreadcrumbValue } from './components/sidebar/RouterBreadcrumb'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface StaticDataRouteOption {
|
||||
breadcrumb?: BreadcrumbValue
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new router instance
|
||||
export const getRouter = () => {
|
||||
const rqContext = TanstackQuery.getContext()
|
||||
|
||||
18
src/routes/(app)/(auth)/change-password.tsx
Normal file
18
src/routes/(app)/(auth)/change-password.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import ChangePasswordForm from '@/components/form/change-password-form'
|
||||
import i18n from '@/lib/i18n'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/(app)/(auth)/change-password')({
|
||||
component: RouteComponent,
|
||||
staticData: { breadcrumb: i18n.t('nav.change_password') },
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<div className="@container/main flex flex-1 flex-col gap-2 p-4">
|
||||
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card *:data-[slot=card]:bg-gradient-to-br *:data-[slot=card]:shadow-xs grid grid-cols-1 @xl/main:grid-cols-2 @5xl/main:grid-cols-3 gap-4">
|
||||
<ChangePasswordForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
9
src/routes/(app)/(auth)/dashboard.tsx
Normal file
9
src/routes/(app)/(auth)/dashboard.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/(app)/(auth)/dashboard')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/(app)/dashboard"!</div>
|
||||
}
|
||||
18
src/routes/(app)/(auth)/profile.tsx
Normal file
18
src/routes/(app)/(auth)/profile.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import ProfileForm from '@/components/form/profile-form'
|
||||
import i18n from '@/lib/i18n'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/(app)/(auth)/profile')({
|
||||
component: RouteComponent,
|
||||
staticData: { breadcrumb: i18n.t('nav.profile') },
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<div className="@container/main flex flex-1 flex-col gap-2 p-4">
|
||||
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card *:data-[slot=card]:bg-gradient-to-br *:data-[slot=card]:shadow-xs grid grid-cols-1 @xl/main:grid-cols-2 @5xl/main:grid-cols-3 gap-4">
|
||||
<ProfileForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { authMiddleware } from '@/lib/middleware'
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/(app)/dashboard')({
|
||||
export const Route = createFileRoute('/(app)/(auth)')({
|
||||
beforeLoad: async ({ context }) => {
|
||||
if (!context.userSession) {
|
||||
throw redirect({ to: '/sign-in' })
|
||||
@@ -14,5 +14,5 @@ export const Route = createFileRoute('/(app)/dashboard')({
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/(app)/dashboard"!</div>
|
||||
return <Outlet />
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import i18n from '@/lib/i18n'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/(app)/')({ component: App })
|
||||
export const Route = createFileRoute('/(app)/')({
|
||||
component: App,
|
||||
staticData: { breadcrumb: i18n.t('nav.home') },
|
||||
})
|
||||
|
||||
function App() {
|
||||
return <div className="min-h-screen bg-linear-to-b ">Home</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ export const Route = createFileRoute('/(app)')({
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<SidebarProvider defaultOpen={true}>
|
||||
<SidebarProvider defaultOpen={false}>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<Header />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import SignInForm from '@/components/signin-form'
|
||||
import SignInForm from '@/components/form/signin-form'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/(auth)/sign-in')({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import SignupForm from '@/components/signup-form'
|
||||
import SignupForm from '@/components/form/signup-form'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/(auth)/sign-up')({
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import NotFound from '@/components/NotFound'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { sessionQueryOptions } from '@/hooks/use-session'
|
||||
import { setSSRLanguage } from '@/lib/i18n'
|
||||
import {
|
||||
CheckIcon,
|
||||
InfoIcon,
|
||||
WarningIcon,
|
||||
WarningOctagonIcon,
|
||||
} from '@phosphor-icons/react'
|
||||
import { TanStackDevtools } from '@tanstack/react-devtools'
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
HeadContent,
|
||||
Scripts,
|
||||
createRootRouteWithContext,
|
||||
} from '@tanstack/react-router'
|
||||
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import TanStackQueryDevtools from '../integrations/tanstack-query/devtools'
|
||||
import appCss from '../styles.css?url'
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
import React from 'react'
|
||||
import { setSSRLanguage } from '@/lib/i18n'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { HugeiconsIcon } from '@hugeicons/react'
|
||||
import {
|
||||
Alert02Icon,
|
||||
AlertCircleIcon,
|
||||
CheckmarkCircle01Icon,
|
||||
InformationCircleIcon,
|
||||
} from '@hugeicons/core-free-icons'
|
||||
import { sessionQueryOptions } from '@/hooks/use-session'
|
||||
|
||||
interface MyRouterContext {
|
||||
queryClient: QueryClient
|
||||
@@ -44,6 +44,9 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||
{
|
||||
title: 'Fuware',
|
||||
},
|
||||
{
|
||||
description: 'Fuware is a platform for managing your business.',
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
@@ -53,7 +56,7 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||
],
|
||||
}),
|
||||
shellComponent: RootDocument,
|
||||
notFoundComponent: () => <div>404 Not Found</div>,
|
||||
notFoundComponent: () => <NotFound />,
|
||||
})
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
@@ -80,34 +83,10 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
},
|
||||
}}
|
||||
icons={{
|
||||
success: (
|
||||
<HugeiconsIcon
|
||||
icon={CheckmarkCircle01Icon}
|
||||
className="text-green-500"
|
||||
size={16}
|
||||
/>
|
||||
),
|
||||
error: (
|
||||
<HugeiconsIcon
|
||||
icon={AlertCircleIcon}
|
||||
className="text-red-500"
|
||||
size={16}
|
||||
/>
|
||||
),
|
||||
info: (
|
||||
<HugeiconsIcon
|
||||
icon={InformationCircleIcon}
|
||||
className="text-blue-500"
|
||||
size={16}
|
||||
/>
|
||||
),
|
||||
warning: (
|
||||
<HugeiconsIcon
|
||||
icon={Alert02Icon}
|
||||
className="text-yellow-500"
|
||||
size={16}
|
||||
/>
|
||||
),
|
||||
success: <CheckIcon className="text-green-500" size={16} />,
|
||||
error: <WarningOctagonIcon className="text-red-500" size={16} />,
|
||||
info: <InfoIcon className="text-blue-500" size={16} />,
|
||||
warning: <WarningIcon className="text-yellow-500" size={16} />,
|
||||
}}
|
||||
/>
|
||||
<React.Suspense>
|
||||
|
||||
17
src/server/profile-service.ts
Normal file
17
src/server/profile-service.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { authMiddleware } from '@/lib/middleware'
|
||||
import { saveFile } from '@/utils/disk-storage'
|
||||
import { createServerFn } from '@tanstack/react-start'
|
||||
import z from 'zod'
|
||||
|
||||
export const uploadProfileImage = createServerFn({ method: 'POST' })
|
||||
.middleware([authMiddleware])
|
||||
.inputValidator(z.instanceof(FormData))
|
||||
.handler(async ({ data: formData }) => {
|
||||
const uuid = crypto.randomUUID()
|
||||
const file = formData.get('file') as File
|
||||
if (!(file instanceof File)) throw new Error('File not found')
|
||||
const imageKey = `${uuid}.${file.type.split('/')[1]}`
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
await saveFile(imageKey, buffer)
|
||||
return { imageKey }
|
||||
})
|
||||
@@ -19,13 +19,13 @@ code {
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.13 0.028 261.692);
|
||||
--foreground: oklch(0.147 0.004 49.25);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.13 0.028 261.692);
|
||||
--card-foreground: oklch(0.147 0.004 49.25);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.13 0.028 261.692);
|
||||
--primary: oklch(0.61 0.11 222);
|
||||
--primary-foreground: oklch(0.98 0.02 201);
|
||||
--popover-foreground: oklch(0.147 0.004 49.25);
|
||||
--primary: oklch(0.646 0.222 41.116);
|
||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.003 264.542);
|
||||
@@ -34,34 +34,34 @@ code {
|
||||
--accent-foreground: oklch(0.21 0.034 264.665);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.928 0.006 264.531);
|
||||
--input: oklch(0.928 0.006 264.531);
|
||||
--ring: oklch(0.707 0.022 261.325);
|
||||
--chart-1: oklch(0.87 0.12 207);
|
||||
--chart-2: oklch(0.80 0.13 212);
|
||||
--chart-3: oklch(0.71 0.13 215);
|
||||
--chart-4: oklch(0.61 0.11 222);
|
||||
--chart-5: oklch(0.52 0.09 223);
|
||||
--border: oklch(0.923 0.003 48.717);
|
||||
--input: oklch(0.923 0.003 48.717);
|
||||
--ring: oklch(0.709 0.01 56.259);
|
||||
--chart-1: oklch(0.837 0.128 66.29);
|
||||
--chart-2: oklch(0.705 0.213 47.604);
|
||||
--chart-3: oklch(0.646 0.222 41.116);
|
||||
--chart-4: oklch(0.553 0.195 38.402);
|
||||
--chart-5: oklch(0.47 0.157 37.304);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0.002 247.839);
|
||||
--sidebar-foreground: oklch(0.13 0.028 261.692);
|
||||
--sidebar-primary: oklch(0.61 0.11 222);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.02 201);
|
||||
--sidebar: oklch(0.985 0.001 106.423);
|
||||
--sidebar-foreground: oklch(0.147 0.004 49.25);
|
||||
--sidebar-primary: oklch(0.646 0.222 41.116);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-accent: oklch(0.967 0.003 264.542);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.034 264.665);
|
||||
--sidebar-border: oklch(0.928 0.006 264.531);
|
||||
--sidebar-ring: oklch(0.707 0.022 261.325);
|
||||
--sidebar-border: oklch(0.923 0.003 48.717);
|
||||
--sidebar-ring: oklch(0.709 0.01 56.259);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.13 0.028 261.692);
|
||||
--foreground: oklch(0.985 0.002 247.839);
|
||||
--card: oklch(0.21 0.034 264.665);
|
||||
--card-foreground: oklch(0.985 0.002 247.839);
|
||||
--popover: oklch(0.21 0.034 264.665);
|
||||
--popover-foreground: oklch(0.985 0.002 247.839);
|
||||
--primary: oklch(0.71 0.13 215);
|
||||
--primary-foreground: oklch(0.30 0.05 230);
|
||||
--background: oklch(0.147 0.004 49.25);
|
||||
--foreground: oklch(0.985 0.001 106.423);
|
||||
--card: oklch(0.216 0.006 56.043);
|
||||
--card-foreground: oklch(0.985 0.001 106.423);
|
||||
--popover: oklch(0.216 0.006 56.043);
|
||||
--popover-foreground: oklch(0.985 0.001 106.423);
|
||||
--primary: oklch(0.705 0.213 47.604);
|
||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.278 0.033 256.848);
|
||||
@@ -69,23 +69,23 @@ code {
|
||||
--accent: oklch(0.278 0.033 256.848);
|
||||
--accent-foreground: oklch(0.985 0.002 247.839);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--destructive-foreground: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.551 0.027 264.364);
|
||||
--chart-1: oklch(0.87 0.12 207);
|
||||
--chart-2: oklch(0.80 0.13 212);
|
||||
--chart-3: oklch(0.71 0.13 215);
|
||||
--chart-4: oklch(0.61 0.11 222);
|
||||
--chart-5: oklch(0.52 0.09 223);
|
||||
--sidebar: oklch(0.21 0.034 264.665);
|
||||
--sidebar-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-primary: oklch(0.80 0.13 212);
|
||||
--sidebar-primary-foreground: oklch(0.30 0.05 230);
|
||||
--sidebar-accent: oklch(0.278 0.033 256.848);
|
||||
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
|
||||
--ring: oklch(0.553 0.013 58.071);
|
||||
--chart-1: oklch(0.837 0.128 66.29);
|
||||
--chart-2: oklch(0.705 0.213 47.604);
|
||||
--chart-3: oklch(0.646 0.222 41.116);
|
||||
--chart-4: oklch(0.553 0.195 38.402);
|
||||
--chart-5: oklch(0.47 0.157 37.304);
|
||||
--sidebar: oklch(0.216 0.006 56.043);
|
||||
--sidebar-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-primary: oklch(0.705 0.213 47.604);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-accent: oklch(0.705 0.213 47.604);
|
||||
--sidebar-accent-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||
--sidebar-ring: oklch(0.553 0.013 58.071);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -118,6 +118,9 @@ code {
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--radius-3xl: calc(var(--radius) + 12px);
|
||||
--radius-4xl: calc(var(--radius) + 16px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
|
||||
25
src/utils/disk-storage.ts
Normal file
25
src/utils/disk-storage.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import fs, { writeFile } from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
export async function saveFile(key: string, file: Buffer | File) {
|
||||
const uploadDir = './files'
|
||||
if (!uploadDir) {
|
||||
throw new Error('Upload directory not found')
|
||||
}
|
||||
|
||||
const fileBuffer =
|
||||
file instanceof File ? Buffer.from(await file.arrayBuffer()) : file
|
||||
|
||||
const filePath = path.join(uploadDir, key)
|
||||
|
||||
try {
|
||||
await fs.mkdir(uploadDir, { recursive: true })
|
||||
await writeFile(filePath, fileBuffer)
|
||||
return key
|
||||
} catch (error) {
|
||||
console.error(`Error saving file: ${key}`, error)
|
||||
throw new Error(
|
||||
`Failed to save file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user