added Profile Page and Change password (also included breadcrumb

This commit is contained in:
2025-12-27 14:46:21 +07:00
parent bd71b27376
commit ba52869e8f
49 changed files with 11108 additions and 12778 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ count.txt
.output
.vinxi
todos.json
files

View File

@@ -12,7 +12,7 @@
"*.css": "tailwindcss"
},
"editor.codeActionsOnSave": {
// "source.organizeImports": "always",
"source.organizeImports": "always",
"source.fixAll": "always"
},
"editor.suggest.preview": true,

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,9 @@
/** @type {import('prettier').Config} */
const config = {
semi: false,
semi: true,
singleQuote: true,
trailingComma: "all",
};
trailingComma: 'all',
}
export default config;

View 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;

View File

@@ -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> */}
</>

View 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

View File

@@ -19,5 +19,6 @@ export function useAuth() {
if (!ctx) {
throw new Error('useAuth must be used within an AuthProvider')
}
return ctx
}

View 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

View 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;

View File

@@ -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>

View File

@@ -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>

View 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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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"

View File

@@ -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 }

View File

@@ -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>
)

View File

@@ -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}
/>

View File

@@ -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}
/>
)
}

View File

@@ -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}

View File

@@ -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",

View File

@@ -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}
/>

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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={

View File

@@ -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 }

View File

@@ -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',
},
})
});
},
},
},
},
})
});

View File

@@ -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;
},
)
);

View File

@@ -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!"
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -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,
}

View File

@@ -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()

View 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>
)
}

View 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>
}

View 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>
)
}

View File

@@ -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 />
}

View File

@@ -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>

View File

@@ -11,7 +11,7 @@ export const Route = createFileRoute('/(app)')({
function RouteComponent() {
return (
<AuthProvider>
<SidebarProvider defaultOpen={true}>
<SidebarProvider defaultOpen={false}>
<AppSidebar />
<SidebarInset>
<Header />

View File

@@ -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')({

View File

@@ -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')({

View File

@@ -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>

View 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 }
})

View File

@@ -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
View 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'}`,
)
}
}