added tons of features

This commit is contained in:
2025-02-05 08:01:14 +02:00
parent 4ae0d8c545
commit 8138da6b1d
195 changed files with 12619 additions and 415 deletions

View File

@@ -0,0 +1,26 @@
import {UserRole} from '@prisma/client'
import {notFound} from 'next/navigation'
import {auth} from '@/auth'
import LoginForm from '@/components/auth/forms/login-form'
import {SessionUser} from '@/types/auth'
export default async function AdminPermission() {
const session = await auth()
const user: SessionUser = session?.user as unknown as SessionUser
if (!user) {
return (
<div className='my-8'>
<div className='container flex flex-col sm:flex-row'>
<LoginForm />
</div>
</div>
)
}
//if (![UserRole.CUSTOMER].includes(user.role as 'CUSTOMER')) {
if (user.role !== UserRole.SUPERVISOR) {
notFound()
}
}

View File

@@ -0,0 +1,171 @@
'use client'
//https://codesandbox.io/p/sandbox/react-hook-form-zod-with-array-of-objects-field-array-usefieldarray-8xh3ry?file=%2Fsrc%2FApp.tsx%3A11%2C53
// https://stackoverflow.com/questions/78004655/how-to-dynamically-add-array-of-objects-to-react-hook-form
import {zodResolver} from '@hookform/resolvers/zod'
import {useState} from 'react'
import {useFieldArray, useForm} from 'react-hook-form'
import {z} from 'zod'
import {onCategoryCreateAction} from '@/actions/admin/category'
import FormError from '@/components/auth/form-error'
import {FormSuccess} from '@/components/auth/form-success'
import {createCategoryFormSchema as validationSchema} from '@/lib/schemas/admin/category'
import {dump} from '@/lib/utils'
import {ResourceMessages} from '@/types'
import {Button} from '@/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/ui/form'
import {Input} from '@/ui/input'
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/ui/tabs'
export const CreateForm = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const localesValues = {
title: '',
short_title: '',
description: ''
}
const form = useForm<z.infer<typeof validationSchema>>({
resolver: zodResolver(validationSchema),
mode: 'onBlur',
defaultValues: {
locales: [
{lang: 'uk', ...localesValues},
{lang: 'ru', ...localesValues}
]
}
})
const {fields, append} = useFieldArray({
name: 'locales',
control: form.control
})
const onSubmit = async (data: z.infer<typeof validationSchema>) => {
setLoading(true)
onCategoryCreateAction(data).then((res: ResourceMessages) => {
if (res?.error) {
setError(res?.error)
setSuccess('')
setLoading(false)
} else {
setSuccess(res?.success as string)
setError('')
setLoading(false)
}
})
}
return (
<Form {...form}>
<form action='' className='my-8' onSubmit={form.handleSubmit(onSubmit)}>
<div className='mx-auto w-[400px]'>
<Tabs defaultValue='uk'>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger value='uk'>Українська</TabsTrigger>
<TabsTrigger value='ru'>російська</TabsTrigger>
</TabsList>
{fields.map((_, index) => (
<TabsContent
value={form.getValues(`locales.${index}.lang`)}
key={index}
className='space-y-8'
>
<FormField
control={form.control}
key={index}
name={`locales.${index}.lang`}
render={({field}) => (
<FormItem className={'w-full'}>
{/*<FormLabel>Мова</FormLabel>*/}
<FormControl>
<Input type='hidden' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 1}
name={`locales.${index}.title`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Назва категорії</FormLabel>
<FormControl>
<Input
lang={form.getValues(`locales.${index}.lang`)}
placeholder=''
{...field}
/>
</FormControl>
{/*<FormDescription>
Select a language between uk or ru
</FormDescription>*/}
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 2}
name={`locales.${index}.short_title`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Скорочена назва категорії</FormLabel>
<FormControl>
<Input
lang={form.getValues(`locales.${index}.lang`)}
placeholder=''
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 3}
name={`locales.${index}.description`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Опис категорії</FormLabel>
<FormControl>
<Input
lang={form.getValues(`locales.${index}.lang`)}
placeholder=''
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
))}
</Tabs>
<div className='my-8'>
<FormError message={error} />
<FormSuccess message={success} />
<Button type='submit'>
{loading ? 'Додаємо до бази...' : 'Створити'}
</Button>
</div>
</div>
</form>
</Form>
)
}

View File

@@ -0,0 +1,47 @@
import {DropdownMenuTrigger} from '@radix-ui/react-dropdown-menu'
import {ChevronUp, User2} from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem
} from '@/ui/dropdown-menu'
import {
SidebarFooter,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem
} from '@/ui/sidebar'
export default function AdminSidebarFooter() {
return (
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton>
<User2 /> Username
<ChevronUp className='ml-auto' />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side='top'
className='w-[--radix-popper-anchor-width]'
>
<DropdownMenuItem>
<span>Account</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>Billing</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>Sign out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
)
}

View File

@@ -0,0 +1,41 @@
import {ChevronDown} from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/ui/dropdown-menu'
import {
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem
} from '@/ui/sidebar'
export default function AdminSidebarHeader() {
return (
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton>
Select Workspace
<ChevronDown className='ml-auto' />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent className='w-[--radix-popper-anchor-width]'>
<DropdownMenuItem>
<span>Acme Inc</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>Acme Corp.</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
)
}

View File

@@ -0,0 +1,442 @@
'use client'
import {zodResolver} from '@hookform/resolvers/zod'
import dynamic from 'next/dynamic'
import React, {useEffect, useMemo, useRef, useState} from 'react'
import {useFieldArray, useForm} from 'react-hook-form'
import {z} from 'zod'
import {onProductCreateAction} from '@/actions/admin/product'
import {useToast} from '@/hooks/use-toast'
import {i18nDefaultLocale, i18nLocales} from '@/i18n-config'
import {BaseEditorConfig} from '@/lib/config/editor'
import {createProductFormSchema} from '@/lib/schemas/admin/product'
import {toEmptyParams} from '@/lib/utils'
import {Button} from '@/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/ui/form'
import {Input} from '@/ui/input'
import {Switch} from '@/ui/switch'
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/ui/tabs'
const JoditEditor = dynamic(() => import('jodit-react'), {ssr: false})
let localesValues = {
title: '',
shortTitle: '',
headingTitle: '',
description: '',
content: '',
instruction: ''
}
let metaValues = {
title: '',
description: '',
keywords: '',
author: ''
}
export default function ProductCreateEditForm({data}: {data?: any}) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const [description0, setDescription0] = useState(
data?.locales[0].description || ''
)
const [description1, setDescription1] = useState(
data?.locales[1].description || ''
)
const [content0, setContent0] = useState(data?.locales[0].content || '')
const [content1, setContent1] = useState(data?.locales[1].content || '')
const [instruction0, setInstruction0] = useState(
data?.locales[0].instruction || ''
)
const [instruction1, setInstruction1] = useState(
data?.locales[1].instruction || ''
)
const editor = useRef(null) //declared a null value
const {toast} = useToast()
const config = useMemo(() => BaseEditorConfig, [])
const form = useForm<z.infer<typeof createProductFormSchema>>({
resolver: zodResolver(createProductFormSchema),
mode: 'onBlur',
defaultValues: data
? (data => {
const {locales, meta} = data
return {
published: data.toStore[0].published,
price: data.toStore[0].price,
pricePromotional: data.toStore[0].pricePromotional,
image: data.image,
locales: toEmptyParams(locales) as any,
meta: meta
? (toEmptyParams(meta) as any)
: [{...metaValues}, {...metaValues}]
}
})(data)
: {
published: false,
price: '0',
pricePromotional: '0',
image: '',
locales: [
{lang: 'uk', ...localesValues},
{lang: 'ru', ...localesValues}
],
meta: [{...metaValues}, {...metaValues}]
}
})
const {register, setValue} = form
useEffect(() => {
register('locales.0.description')
register('locales.0.content')
register('locales.0.instruction')
register('locales.1.description')
register('locales.1.content')
register('locales.1.instruction')
}, [register])
const {fields: localeFields} = useFieldArray({
name: 'locales',
control: form.control
})
const {fields: metaFields} = useFieldArray({
name: 'meta',
control: form.control
})
console.log(form.formState.errors)
const onSubmit = async (values: z.infer<typeof createProductFormSchema>) => {
setLoading(true)
onProductCreateAction(values).then((res: any) => {
if (res?.error) {
setError(res?.error)
setSuccess('')
setLoading(false)
toast({
variant: 'destructive',
title: res?.error,
description: res?.message
})
} else {
setSuccess(res?.success as string)
setError('')
setLoading(false)
toast({
variant: 'success',
title: res?.success,
description: res?.message
})
}
})
}
return (
<Form {...form}>
<form
action=''
onSubmit={form.handleSubmit(onSubmit)}
className='bgs-grasy-50/50 mx-auto mb-8 min-w-[640px] max-w-[992px] flex-1 space-y-5 rounded-lg border p-4'
>
<div className='mx-auto my-4 w-full space-y-4'>
<h1 className='mb-6 text-center text-2xl font-bold text-brand-violet'>
ДОДАВАННЯ ТОВАРУ ДО БАЗИ
</h1>
<div className='my-4 space-y-4'>
<FormField
control={form.control}
name='published'
render={({field}) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border bg-gray-50 p-4'>
<div className='space-y-0.5'>
<FormLabel className='text-base'>Опублікувати</FormLabel>
<FormDescription>
Відразу після збереження буде розміщено на сайті
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<div className='flex flex-row items-center justify-between gap-4 rounded-lg border bg-gray-50 p-4'>
<div className='w-1/2'>
<FormField
control={form.control}
name='price'
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Ціна за одиницю товару</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='w-1/2'>
<FormField
control={form.control}
name='pricePromotional'
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Акційна Ціна</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<fieldset className='flex gap-4 rounded-lg border bg-gray-50 p-4'>
<FormField
control={form.control}
name='image'
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Головне зображення</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormDescription>
Вкажіть шліх до зображення відносно публічної папки
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</fieldset>
</div>
<Tabs
defaultValue={i18nDefaultLocale}
className='min-h-[560px] rounded-lg border p-4'
>
<TabsList className='grid w-full grid-cols-2'>
{i18nLocales.map(locale => (
<TabsTrigger key={locale.icon} value={locale.code}>
{locale.nameUkr}
</TabsTrigger>
))}
</TabsList>
{localeFields.map((_, index) => (
<TabsContent
id={`form-tab-${form.getValues(`locales.${index}.lang`)}`}
value={form.getValues(`locales.${index}.lang`)}
key={index}
className='space-y-4'
>
<FormField
control={form.control}
key={index}
name={`locales.${index}.lang`}
render={({field}) => (
<FormItem className={'w-full'}>
{/*<FormLabel>Мова</FormLabel>*/}
<FormControl>
<Input type='hidden' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<fieldset className='flex gap-4 rounded-lg border bg-gray-50 p-4'>
<div className='w-1/2'>
<FormField
control={form.control}
key={index + 1}
name={`locales.${index}.title`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Назва товару</FormLabel>
<FormControl>
<Input placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='w-1/2'>
<FormField
control={form.control}
key={index + 2}
name={`locales.${index}.shortTitle`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Скорочена назва товару</FormLabel>
<FormControl>
<Input placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</fieldset>
<fieldset className='rounded-lg border bg-gray-50 p-4'>
<FormField
control={form.control}
key={index + 3}
name={`locales.${index}.headingTitle`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>
Назва товару у описі та коротка анотація
</FormLabel>
<FormControl>
<Input placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<JoditEditor
key={index + 4}
ref={editor}
config={config}
value={index === 0 ? description0 : description1}
className='mt-4 w-full'
onBlur={value => {
index === 0
? setDescription0(value)
: setDescription1(value)
setValue(`locales.${index}.description`, value)
}}
/>
</fieldset>
<fieldset className='rounded-lg border bg-gray-50 p-4'>
<FormLabel>Опис товару</FormLabel>
<JoditEditor
key={index + 5}
ref={editor}
config={config}
value={index === 0 ? content0 : content1}
className='mt-4 w-full'
onBlur={value => {
index === 0 ? setContent0(value) : setContent1(value)
setValue(`locales.${index}.content`, value)
}}
/>
</fieldset>
<fieldset className='rounded-lg border bg-gray-50 p-4'>
<FormLabel>Інструкція до товару</FormLabel>
<JoditEditor
key={index + 2125}
ref={editor}
config={config}
value={index === 0 ? instruction0 : instruction1}
className='mt-4 w-full'
onBlur={value => {
index === 0
? setInstruction0(value)
: setInstruction1(value)
setValue(`locales.${index}.instruction`, value)
}}
/>
</fieldset>
</TabsContent>
))}
{metaFields.map((_, index) => (
<TabsContent
id={`form-tab-${form.getValues(`locales.${index}.lang`)}`}
value={form.getValues(`locales.${index}.lang`)}
key={index}
className='space-y-4'
>
<fieldset className='rounded-lg border bg-gray-50 p-4'>
<legend className='rounded-lg border bg-gray-200 px-16 py-1 text-xl font-bold'>
META ДАНІ
</legend>
<FormField
control={form.control}
key={index + 'meta.title'}
name={`meta.${index}.title`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Назва</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 'meta.description'}
name={`meta.${index}.description`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Опис</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 'meta.keywords'}
name={`meta.${index}.keywords`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Ключові слова</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 'meta.author'}
name={`meta.${index}.author`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Автор</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</fieldset>
</TabsContent>
))}
</Tabs>
</div>
<Button type='submit' className='!mt-0 w-full'>
Створити
</Button>
</form>
</Form>
)
}

View File

@@ -0,0 +1,169 @@
'use client'
import {zodResolver} from '@hookform/resolvers/zod'
import fs from 'node:fs'
import path from 'node:path'
import {useCallback, useState} from 'react'
import Dropzone from 'react-dropzone'
import {useFieldArray, useForm} from 'react-hook-form'
import {z} from 'zod'
import {onProductCreateAction} from '@/actions/admin/product'
import {createCategoryFormSchema} from '@/lib/schemas/admin/category'
import {createProductFormSchema} from '@/lib/schemas/admin/product'
import {cn, dump} from '@/lib/utils'
import {ResourceMessages} from '@/types'
import {Button} from '@/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/ui/form'
import {Input} from '@/ui/input'
export default function CreateForm() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const form = useForm<z.infer<typeof createProductFormSchema>>({
resolver: zodResolver(createProductFormSchema),
mode: 'onBlur',
defaultValues: {
files: []
}
})
const {fields, append} = useFieldArray({
name: 'files',
control: form.control
})
console.log(form.formState.errors)
const onSubmit = async (values: z.infer<typeof createProductFormSchema>) => {
setLoading(true)
onProductCreateAction(values).then((res: any) => {
if (res?.error) {
setError(res?.error)
setSuccess('')
setLoading(false)
} else {
setSuccess(res?.success as string)
setError('')
setLoading(false)
}
})
}
return (
<div className='flex h-screen items-center justify-center'>
<Form {...form}>
<form
action=''
onSubmit={form.handleSubmit(onSubmit)}
className='max-w-md flex-1 space-y-5'
>
<div className='products_name_price_desc relative'>
{fields.map((_, index) => {
return (
<div key={index}>
<div className='mb-2 mt-7 text-xl font-bold'>
{/*{form.getValues(`files.${index}.file.name`)}*/}
{dump(form.getValues(`files.${index}`))}
</div>
<div className='flex gap-x-3'>
<FormField
control={form.control}
key={index}
name={`files.${index}.alt`}
render={({field}) => (
<FormItem>
<FormLabel>Файл Альт Ім'я</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage className='capitalize text-red-500' />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 1}
name={`files.${index}.title`}
render={({field}) => (
<FormItem>
<FormLabel>Назва файлу</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage className='capitalize text-red-500' />
</FormItem>
)}
/>
</div>
</div>
)
})}
</div>
<div className='products relative'>
<FormField
control={form.control}
name='files'
render={() => (
<Dropzone
accept={{
'image/*': ['.jpg', '.jpeg', '.png']
}}
onDropAccepted={acceptedFiles => {
acceptedFiles.map(acceptedFile => {
console.log('acceptedFile', acceptedFile)
return append({
file: acceptedFile,
alt: '',
title: ''
})
})
}}
multiple={true}
maxSize={5000000}
>
{({getRootProps, getInputProps}) => (
<section>
<div {...getRootProps()}>
<input {...getInputProps()} />
<p>
Перетягніть тут, киньте кілька файлів або натисніть,
щоб вибрати файли
</p>
</div>
</section>
)}
</Dropzone>
)}
/>
</div>
<Button type='submit' className='!mt-0 w-full'>
Створити
</Button>
</form>
</Form>
</div>
)
// return (
// <div className='flex flex-col bg-zinc-200 py-10'>
// <h1 className='text-center text-3xl font-bold capitalize'>
// Створити галерею
// </h1>
// <div className='mx-auto mb-10 mt-6 flex min-h-[320px] w-[80%] flex-wrap gap-1 rounded-md bg-white p-5 shadow-sm'></div>
// <div className='flex justify-center'>
// <Button>Завантажити зображення</Button>
// </div>
// </div>
// )
}

View File

@@ -0,0 +1,106 @@
import {
ChevronDown,
Home,
Inbox,
LayoutList,
Plus,
ScanBarcode,
Search,
Settings
} from 'lucide-react'
import AdminSidebarFooter from '@/components/(protected)/admin/footer'
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem
} from '@/components/ui/sidebar'
import {ADMIN_DASHBOARD_PATH} from '@/lib/config/routes'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from '@/ui/collapsible'
const items = [
{
title: 'Головна',
url: `${ADMIN_DASHBOARD_PATH}`,
icon: Home
},
{
title: 'Категорії',
url: `${ADMIN_DASHBOARD_PATH}/category`,
icon: LayoutList
},
{
title: 'Товари',
url: `${ADMIN_DASHBOARD_PATH}/product`,
icon: ScanBarcode
}
// {
// title: 'Search',
// url: '#',
// icon: Search
// },
// {
// title: 'Settings',
// url: '#',
// icon: Settings
// }
]
export function AdminSidebar() {
return (
<Sidebar collapsible='icon' variant='sidebar'>
{/*<AdminSidebarHeader />*/}
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel asChild>Projects</SidebarGroupLabel>
<SidebarGroupAction title='Add Project'>
<Plus /> <span className='sr-only'>Add Project</span>
</SidebarGroupAction>
<SidebarGroupContent>SidebarGroupAction</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map(item => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<Collapsible defaultOpen className='group/collapsible'>
<SidebarGroup>
<SidebarGroupLabel asChild>
<CollapsibleTrigger>
Help
<ChevronDown className='ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180' />
</CollapsibleTrigger>
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>SidebarGroupContent</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
</SidebarContent>
<AdminSidebarFooter />
</Sidebar>
)
}

View File

@@ -0,0 +1,18 @@
import Logo from '@/components/shared/home/logo'
interface HeaderProps {
label: string
title: string
}
const AuthHeader = ({title, label}: HeaderProps) => {
return (
<div className='flex w-full flex-col items-center justify-center gap-y-4'>
<Logo />
<h1 className='text-3xl font-semibold'>{title}</h1>
<p className='text-sm text-muted-foreground'>{label}</p>
</div>
)
}
export default AuthHeader

View File

@@ -0,0 +1,16 @@
import Link from 'next/link'
import {Button} from '@/components/ui/button'
interface BackButtonProps {
label: string
href: string
}
export const BackButton = ({label, href}: BackButtonProps) => {
return (
<Button variant='link' className='w-full font-normal' size='sm' asChild>
<Link href={href}>{label}</Link>
</Button>
)
}

View File

@@ -0,0 +1,37 @@
import {ReactNode} from 'react'
import AuthHeader from './auth-header'
import {BackButton} from './back-button'
import {Card, CardContent, CardFooter, CardHeader} from '@/components/ui/card'
interface CardWrapperProps {
children: ReactNode
headerLabel: string
backButtonLabel: string
title: string
showSocial?: boolean
backButtonHref: string
}
const CardWrapper = ({
children,
headerLabel,
backButtonLabel,
backButtonHref,
title,
showSocial
}: CardWrapperProps) => {
return (
<Card className='m-auto shadow-md md:w-1/2 xl:w-1/4'>
<CardHeader>
<AuthHeader label={headerLabel} title={title} />
</CardHeader>
<CardContent>{children}</CardContent>
<CardFooter>
<BackButton label={backButtonLabel} href={backButtonHref} />
</CardFooter>
</Card>
)
}
export default CardWrapper

View File

@@ -0,0 +1,17 @@
import {ShieldAlert} from 'lucide-react'
interface FormSuccessProps {
message?: string
}
export const FormError = ({message}: FormSuccessProps) => {
if (!message) return null
return (
<div className='flex items-center space-x-4 rounded-lg bg-red-500/30 p-2 text-red-500'>
<ShieldAlert size={16} />
<p>{message}</p>
</div>
)
}
export default FormError

View File

@@ -0,0 +1,15 @@
import {CheckCheckIcon} from 'lucide-react'
interface FormSuccessProps {
message?: string
}
export const FormSuccess = ({message}: FormSuccessProps) => {
if (!message) return null
return (
<div className='flex items-center space-x-4 rounded-lg bg-emerald-500/30 p-2 text-emerald-500'>
<CheckCheckIcon className='h-4 w-4' />
<p>{message}</p>
</div>
)
}

View File

@@ -0,0 +1,104 @@
'use client'
import {zodResolver} from '@hookform/resolvers/zod'
import {useState} from 'react'
import {useForm} from 'react-hook-form'
import {z} from 'zod'
import {login} from '@/actions/auth/login'
import CardWrapper from '@/components/auth/card-wrapper'
import {FormError} from '@/components/auth/form-error'
import GoogleLogin from '@/components/auth/google-login'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import {LoginSchema} from '@/lib/schemas'
import {Button} from '@/ui/button'
import {Input} from '@/ui/input'
export default function LoginForm() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const form = useForm<z.infer<typeof LoginSchema>>({
resolver: zodResolver(LoginSchema),
defaultValues: {
email: '',
password: ''
}
})
const onSubmit = async (data: z.infer<typeof LoginSchema>) => {
setLoading(true)
login(data).then(res => {
if (res?.error) {
setError(res?.error)
setLoading(false)
} else {
setError('')
setLoading(false)
}
})
}
return (
<CardWrapper
headerLabel='Create an account'
title='Register'
backButtonHref='/auth/login'
backButtonLabel='Already have an account'
showSocial
>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className='m-auto space-y-6'
>
<div className='space-y-4'>
<FormField
control={form.control}
name='email'
render={({field}) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
{...field}
placeholder='johndoe@email.com'
type='email'
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
render={({field}) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input {...field} placeholder='******' type='password' />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormError message={error} />
<Button type='submit' className='w-full' disabled={loading}>
{loading ? 'Loading...' : 'Login'}
</Button>
</form>
</Form>
<GoogleLogin />
</CardWrapper>
)
}

View File

@@ -0,0 +1,133 @@
'use client'
import {zodResolver} from '@hookform/resolvers/zod'
import {useState} from 'react'
import {useForm} from 'react-hook-form'
import {z} from 'zod'
import {register} from '@/actions/auth/register'
import CardWrapper from '@/components/auth/card-wrapper'
import {FormError} from '@/components/auth/form-error'
import {FormSuccess} from '@/components/auth/form-success'
import GoogleLogin from '@/components/auth/google-login'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import {RegisterSchema} from '@/lib/schemas'
import {Button} from '@/ui/button'
import {Input} from '@/ui/input'
export default function RegisterForm() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const form = useForm<z.infer<typeof RegisterSchema>>({
resolver: zodResolver(RegisterSchema),
defaultValues: {
email: '',
name: '',
password: '',
passwordConfirmation: ''
}
})
const onSubmit = async (data: z.infer<typeof RegisterSchema>) => {
setLoading(true)
register(data).then(res => {
if (res.error) {
setError(res.error)
setLoading(false)
}
if (res.success) {
setError('')
setSuccess(res.success)
setLoading(false)
}
})
}
return (
<CardWrapper
headerLabel='Create an account'
title='Register'
backButtonHref='/auth/login'
backButtonLabel='Already have an account'
showSocial
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
<div className='space-y-4'>
<FormField
control={form.control}
name='email'
render={({field}) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
{...field}
placeholder='johndoe@email.com'
type='email'
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='name'
render={({field}) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} placeholder='John Doe' />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
render={({field}) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input {...field} placeholder='******' type='password' />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='passwordConfirmation'
render={({field}) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input {...field} placeholder='******' type='password' />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormSuccess message={success} />
<FormError message={error} />
<Button type='submit' className='w-full' disabled={loading}>
{loading ? 'Loading...' : 'Register'}
</Button>
</form>
</Form>
{/*<GoogleLogin />*/}
</CardWrapper>
)
}

View File

@@ -0,0 +1,14 @@
import {signOut} from '@/auth'
export function SignOutButton() {
return (
<form
action={async () => {
'use server'
await signOut()
}}
>
<button type='submit'>Sign Out</button>
</form>
)
}

View File

@@ -0,0 +1,29 @@
'use client'
import {useActionState} from 'react'
import {googleAuthenticate} from '@/actions/auth/google-login'
import GoogleIcon from '@/components/shared/icons/google'
import {Button} from '@/ui/button'
const GoogleLogin = () => {
const [errorMsgGoogle, dispatchGoogle] = useActionState(
googleAuthenticate,
undefined
) //googleAuthenticate hook
return (
<form className='mt-4 flex' action={dispatchGoogle}>
<Button
variant={'outline'}
className='flex w-full flex-row items-center gap-3'
>
<GoogleIcon />
Google Sign In
</Button>
<p>{errorMsgGoogle}</p>
</form>
)
}
export default GoogleLogin

View File

@@ -0,0 +1,60 @@
import {useTranslations} from 'next-intl'
import {SignOutButton} from '@/components/auth/forms/sign-out-button'
import CabinetButton from '@/components/shared/header/cabinet-button'
import {type SingedInSession} from '@/lib/permission'
import {dump} from '@/lib/utils'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from '@/ui/collapsible'
import {Separator} from '@/ui/separator'
export default function CabinetIndex({
slug,
session
}: {
slug: string[] | undefined
session: SingedInSession | null
}) {
const t = useTranslations('cabinet')
return (
<div className='my-8'>
<div className='container flex flex-col sm:flex-row'>
<section className='bw-layout-col-left bw-border-color border-r pt-3'>
<div className='flex items-center justify-between pr-4'>
<CabinetButton />
<div>
<p className='text-sm'>{session?.user?.name}</p>
<p className='text-xs'>{session?.user?.email}</p>
</div>
</div>
<Separator className='bw-separator-color my-4' />
<SignOutButton />
</section>
<div className='bw-layout-col-right pt-3'>
<section className='w-full'>
<h1 className='text-3xl font-extrabold'>
{t('personal-information.title')}
</h1>
<Separator className='my-4' />
{/*<BasicEditor placeholder={'type something'} />*/}
{/*<Separator className='my-4' />*/}
{slug ? <code>{slug[0]}</code> : <pre>{dump(session)}</pre>}
<Collapsible>
<CollapsibleTrigger>
Can I use this in my project?
</CollapsibleTrigger>
<CollapsibleContent>
Yes. Free to use for personal and commercial projects. No
attribution required.
</CollapsibleContent>
</Collapsible>
</section>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,19 @@
.cls-1 {
fill: #fff;
}
.cls-2 {
fill: #f05539;
}
.cls-3 {
fill: #b9c2e2;
}
.cls-4 {
fill: #a42f23;
}
.cls-5 {
fill: #ea5151;
}

View File

@@ -0,0 +1,115 @@
import styles from './css/fig-one.module.css'
const ar = 72.79 / 38.95
export default function FigOne() {
return (
<div>
<svg
id='fig-1'
data-name='fig 1'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 72.79 38.95'
>
<g id='Layer_1' data-name='Layer 1'>
<g>
<path d='M63.83,21.76c-.23,0-.47-.02-.71-.06-1.25-.2-2.3-.93-3.08-1.54-1.96-1.55-3.6-3.55-4.72-5.79-.06-.11-.01-.25,.1-.31,.11-.06,.25-.01,.31,.1,1.09,2.18,2.68,4.13,4.59,5.64,1.09,.86,1.97,1.31,2.87,1.45,1.07,.17,2.29-.12,3.46-.81,1.07-.63,1.98-1.52,2.87-2.37,.09-.09,.24-.09,.32,0,.09,.09,.09,.24,0,.32-.9,.87-1.84,1.77-2.95,2.43-.75,.44-1.83,.92-3.05,.92Z' />
<g>
<path
className='cls-1'
d='M71.22,16.24c-1.18-.57-2.6-.08-3.17,1.11-.57,1.18-.08,2.6,1.11,3.17,1.18,.57,2.6,.08,3.17-1.11,.57-1.18,.08-2.6-1.11-3.17Z'
/>
<path d='M70.18,20.99c-.38,0-.77-.08-1.13-.26-1.29-.63-1.84-2.19-1.21-3.48,.3-.63,.83-1.1,1.49-1.33,.66-.23,1.36-.19,1.99,.11,.63,.3,1.1,.83,1.33,1.49,.23,.66,.19,1.36-.11,1.99-.45,.93-1.38,1.47-2.35,1.47Zm0-4.75c-.24,0-.48,.04-.71,.12-.54,.19-.98,.58-1.23,1.09-.52,1.07-.07,2.35,1,2.87,1.07,.52,2.35,.07,2.87-1,.25-.52,.28-1.1,.09-1.64-.19-.54-.58-.98-1.09-1.23h0c-.3-.14-.61-.22-.93-.22Z' />
</g>
<path d='M6.35,23.78c-1.23,0-2.55-.78-3.91-2.33-.08-.09-.07-.24,.02-.32,.09-.08,.24-.07,.32,.02,1.55,1.76,2.99,2.45,4.27,2.07,1.97-.59,3.5-3.72,4.2-8.6,.02-.13,.13-.21,.26-.19,.12,.02,.21,.13,.19,.26-.73,5.06-2.38,8.33-4.53,8.97-.27,.08-.55,.12-.83,.12Z' />
<g>
<path
className='cls-1'
d='M3.64,19.16c-1.18-.57-2.6-.08-3.17,1.11-.57,1.18-.08,2.6,1.11,3.17,1.18,.57,2.6,.08,3.17-1.11,.57-1.18,.08-2.6-1.11-3.17Z'
/>
<path d='M2.6,23.91c-.39,0-.77-.09-1.13-.26-.63-.3-1.1-.83-1.33-1.49-.23-.66-.19-1.36,.11-1.99,.62-1.29,2.19-1.84,3.48-1.21h0c.63,.3,1.1,.83,1.33,1.49,.23,.66,.19,1.36-.11,1.99-.3,.63-.83,1.1-1.49,1.33-.28,.1-.57,.15-.86,.15Zm0-4.75c-.8,0-1.57,.45-1.94,1.21-.25,.52-.28,1.1-.09,1.64,.19,.54,.58,.98,1.09,1.23,.52,.25,1.1,.28,1.64,.09,.54-.19,.98-.58,1.23-1.09,.25-.52,.28-1.1,.09-1.64-.19-.54-.58-.98-1.09-1.23h0c-.3-.15-.62-.21-.93-.21Z' />
</g>
<g>
<path d='M28.61,36.94s0,0,0,0c-.13,0-.23-.11-.22-.24,.14-4.32,.09-8.7-.16-13.01,0-.13,.09-.23,.21-.24,.12,0,.23,.09,.24,.21,.25,4.33,.31,8.72,.16,13.05,0,.12-.11,.22-.23,.22Z' />
<path d='M28.61,36.94s0,0,0,0c-.13,0-.23-.11-.22-.24,.14-4.32,.09-8.7-.16-13.01,0-.13,.09-.23,.21-.24,.12,0,.23,.09,.24,.21,.25,4.33,.31,8.72,.16,13.05,0,.12-.11,.22-.23,.22Z' />
<path d='M42.12,36.94c-.12,0-.22-.1-.23-.22-.14-4.33-.09-8.72,.16-13.05,0-.13,.12-.22,.24-.21,.13,0,.22,.12,.21,.24-.25,4.31-.31,8.69-.16,13.01,0,.13-.09,.23-.22,.24,0,0,0,0,0,0Z' />
<g>
<path
className='cls-1'
d='M26.88,35.49c-.94,.1-1.94,.22-2.59,1-.32,.39-.44,.93-.24,1.39,.31,.72,1.12,.82,1.83,.83,1.07,.02,2.07,.02,3.12-.21,.64-.14,1.02-.52,1.09-2,.02-.53-.39-.99-.93-1.01-.78-.03-1.83-.06-2.29,0Z'
/>
<path d='M26.6,38.95c-.24,0-.48,0-.72,0-.81-.02-1.68-.14-2.04-.97-.23-.52-.12-1.15,.27-1.63,.72-.87,1.84-.99,2.74-1.09h0c.47-.05,1.52-.02,2.32,0,.32,.01,.62,.15,.83,.39,.22,.24,.33,.54,.31,.86-.06,1.43-.41,2.03-1.27,2.22-.84,.18-1.64,.22-2.45,.22Zm.3-3.24c-.86,.09-1.83,.19-2.43,.92-.29,.35-.36,.79-.21,1.16,.25,.59,.93,.68,1.63,.69,1.02,.02,2.03,.02,3.07-.21,.46-.1,.84-.3,.91-1.79,0-.2-.06-.38-.19-.53-.13-.15-.32-.23-.51-.24-1.11-.04-1.91-.05-2.25,0h0Zm-.02-.23h0Z' />
</g>
<g>
<path
className='cls-1'
d='M43.85,35.49c.94,.1,1.94,.22,2.59,1,.32,.39,.44,.93,.24,1.39-.31,.72-1.12,.82-1.83,.83-1.07,.02-2.07,.02-3.12-.21-.64-.14-1.02-.52-1.09-2-.02-.53,.39-.99,.93-1.01,.78-.03,1.83-.06,2.29,0Z'
/>
<path d='M44.12,38.95c-.81,0-1.61-.04-2.45-.22-.86-.19-1.2-.79-1.27-2.22-.01-.32,.1-.62,.31-.86,.22-.24,.51-.37,.83-.39,.8-.03,1.85-.06,2.32,0h0c.9,.09,2.02,.21,2.74,1.09,.39,.48,.5,1.1,.27,1.63-.36,.83-1.23,.95-2.04,.97-.24,0-.48,0-.72,0Zm-1.05-3.26c-.4,0-.91,.01-1.5,.03-.2,0-.38,.09-.51,.24-.13,.15-.2,.33-.19,.53,.07,1.49,.45,1.69,.91,1.79,1.04,.23,2.04,.23,3.07,.21,.7-.01,1.37-.11,1.63-.69,.16-.37,.08-.81-.21-1.16-.6-.73-1.57-.83-2.43-.92h0c-.16-.02-.41-.03-.75-.03Z' />
</g>
</g>
<g>
<path d='M58.22,11.86c-.12-1.22-.31-2.86-.87-3.96-.88-1.74-2.08-2.55-3.92-3.21-1.84-.67-4.34-.86-6.28-1.05-2.7-.26-9.2-.48-12.34-.48s-9.64,.22-12.34,.48c-1.95,.19-4.44,.39-6.28,1.05-1.84,.67-3.03,1.47-3.92,3.21-.56,1.1-.75,2.74-.87,3.96-.12,1.22-.16,2.45-.13,3.67-.03,1.22,.01,2.45,.13,3.67,.12,1.22,.31,2.86,.87,3.96,.88,1.74,2.08,2.55,3.92,3.21,1.84,.67,4.34,.86,6.28,1.05,2.7,.26,8.18,.48,12.34,.48s9.64-.22,12.34-.48c1.95-.19,4.44-.39,6.28-1.05,1.84-.67,3.03-1.47,3.92-3.21,.56-1.1,.75-2.73,.87-3.96,.12-1.22,.16-2.45,.13-3.67,.03-1.22-.01-2.45-.13-3.67Z' />
<path d='M34.81,28.15c-4.1,0-9.65-.22-12.36-.48l-.49-.05c-1.84-.18-4.14-.4-5.85-1.02-2.4-.87-3.35-1.96-4.05-3.33-.55-1.09-.76-2.66-.9-4.05-.12-1.21-.16-2.45-.13-3.7-.03-1.24,.02-2.48,.13-3.69,.13-1.39,.34-2.96,.9-4.05,.96-1.9,2.32-2.7,4.05-3.33,1.72-.62,4.01-.84,5.85-1.02l.49-.05c2.6-.25,9.08-.48,12.36-.48s9.76,.23,12.36,.48l.49,.05c1.84,.18,4.14,.4,5.85,1.02,2.4,.87,3.35,1.96,4.05,3.33,.55,1.09,.76,2.66,.9,4.05,.12,1.21,.16,2.45,.13,3.7,.03,1.24-.02,2.48-.13,3.69-.13,1.39-.34,2.96-.9,4.05-.96,1.9-2.32,2.7-4.05,3.33-1.72,.62-4.01,.84-5.85,1.02l-.49,.05c-2.71,.27-8.26,.48-12.36,.48Zm0-24.75c-3.27,0-9.72,.23-12.32,.48l-.49,.05c-1.82,.17-4.08,.39-5.73,.99-1.67,.61-2.89,1.34-3.78,3.1-.52,1.02-.72,2.53-.84,3.87-.11,1.19-.16,2.41-.13,3.64-.03,1.24,.02,2.46,.13,3.65,.13,1.34,.33,2.86,.84,3.87,.89,1.76,2.11,2.49,3.78,3.1,1.66,.6,3.92,.82,5.73,.99l.49,.05c2.7,.26,8.23,.48,12.32,.48s9.61-.21,12.32-.48l.49-.05c1.82-.17,4.08-.39,5.73-.99,1.67-.61,2.89-1.34,3.78-3.1,.52-1.02,.72-2.53,.84-3.87,.11-1.19,.16-2.41,.13-3.64,.03-1.24-.02-2.46-.13-3.65h0c-.13-1.34-.33-2.86-.84-3.87-.89-1.76-2.11-2.49-3.78-3.1-1.66-.6-3.92-.82-5.73-.99l-.49-.05c-2.59-.25-9.05-.48-12.32-.48Z' />
</g>
<g>
<path
className='cls-3'
d='M58.22,8.95c-.12-1.22-.31-2.86-.87-3.96-.88-1.74-2.08-2.55-3.92-3.21-1.84-.67-4.34-.86-6.28-1.05-2.7-.26-9.2-.48-12.34-.48s-9.64,.22-12.34,.48c-1.95,.19-4.44,.39-6.28,1.05-1.84,.67-3.03,1.47-3.92,3.21-.56,1.1-.75,2.74-.87,3.96-.12,1.22-.16,2.45-.13,3.67-.03,1.22,.01,2.45,.13,3.67,.12,1.22,.31,2.86,.87,3.96,.88,1.74,2.08,2.55,3.92,3.21,1.84,.67,4.34,.86,6.28,1.05,2.7,.26,8.18,.48,12.34,.48s9.64-.22,12.34-.48c1.95-.19,4.44-.39,6.28-1.05,1.84-.67,3.03-1.47,3.92-3.21,.56-1.1,.75-2.74,.87-3.96,.12-1.22,.16-2.45,.13-3.67,.03-1.22-.01-2.45-.13-3.67Z'
/>
<path d='M34.81,25.25c-4.1,0-9.65-.22-12.36-.48l-.49-.05c-1.84-.18-4.14-.4-5.85-1.02-2.4-.87-3.35-1.96-4.05-3.33-.55-1.09-.76-2.66-.9-4.05-.12-1.21-.16-2.45-.13-3.7-.03-1.24,.02-2.48,.13-3.69,.13-1.39,.34-2.96,.9-4.05,.96-1.9,2.32-2.7,4.05-3.33,1.72-.62,4.01-.84,5.85-1.02l.49-.05c2.6-.25,9.08-.48,12.36-.48s9.76,.23,12.36,.48l.49,.05c1.84,.18,4.14,.4,5.85,1.02,2.4,.87,3.35,1.96,4.05,3.33,.55,1.09,.76,2.66,.9,4.05,.12,1.21,.16,2.45,.13,3.7,.03,1.24-.02,2.48-.13,3.69-.13,1.39-.34,2.96-.9,4.05-.96,1.9-2.32,2.7-4.05,3.33-1.72,.62-4.01,.84-5.86,1.02l-.49,.05c-2.71,.27-8.26,.48-12.36,.48ZM34.81,.49c-3.27,0-9.72,.23-12.32,.48l-.49,.05c-1.82,.17-4.07,.39-5.73,.99-1.67,.61-2.89,1.34-3.78,3.1-.52,1.02-.72,2.53-.84,3.87-.11,1.19-.16,2.41-.13,3.64-.03,1.24,.02,2.46,.13,3.65,.13,1.34,.33,2.86,.84,3.87,.89,1.76,2.11,2.49,3.78,3.1,1.66,.6,3.92,.82,5.73,.99l.49,.05c2.7,.26,8.23,.48,12.32,.48s9.61-.21,12.32-.48l.49-.05c1.82-.17,4.08-.39,5.73-.99,1.67-.61,2.89-1.34,3.78-3.1,.52-1.02,.72-2.53,.84-3.87,.11-1.19,.16-2.41,.13-3.64,.03-1.24-.02-2.46-.13-3.65h0c-.13-1.34-.33-2.86-.84-3.87-.89-1.76-2.11-2.49-3.78-3.1-1.66-.6-3.92-.82-5.73-.99l-.49-.05c-2.59-.25-9.05-.48-12.32-.48Z' />
</g>
<path d='M34.81,25.25c-.14,0-.25-.11-.25-.25V.25c0-.14,.11-.25,.25-.25s.25,.11,.25,.25V25c0,.14-.11,.25-.25,.25Z' />
<g>
<path
className='cls-4'
d='M30.17,12.96c-1.08,0-1.65,1.28-.91,2.07,.5,.54,1.05,1.04,1.66,1.49,1.34,.99,3.05,1.59,4.7,1.3,1.11-.2,2.12-.8,3.02-1.47,.51-.38,.99-.79,1.45-1.23,.81-.77,.26-2.13-.85-2.13-2.69,.01-6.52-.01-9.07-.03Z'
/>
<path d='M34.76,18.12c-1.32,0-2.73-.49-3.98-1.42-.6-.45-1.16-.94-1.7-1.52-.4-.43-.51-1.04-.27-1.58,.24-.55,.75-.88,1.35-.88h0c3.24,.02,6.59,.04,9.07,.03h0c.61,0,1.13,.35,1.35,.92,.23,.57,.1,1.18-.35,1.61-.47,.44-.96,.86-1.47,1.25-1.14,.85-2.13,1.34-3.12,1.52-.29,.05-.59,.08-.9,.08Zm-4.6-4.93c-.48,0-.79,.31-.92,.6-.13,.29-.15,.73,.18,1.08,.52,.55,1.05,1.03,1.63,1.46,1.43,1.06,3.08,1.52,4.52,1.25,.92-.17,1.85-.62,2.93-1.43,.5-.37,.98-.78,1.43-1.21,.36-.35,.36-.8,.24-1.1-.12-.3-.43-.63-.93-.63h0c-2.49,.01-5.83,0-9.07-.03h0Z' />
</g>
<g>
<path
className='cls-5'
d='M36.02,16.31c-.64-.17-1.31-.24-1.97-.2-.66,.04-1.32,.2-1.92,.47-.21,.1-.42,.21-.61,.33,1.24,.75,2.7,1.16,4.11,.91,.71-.13,1.38-.42,2.01-.79-.5-.33-1.04-.57-1.61-.72Z'
/>
<path d='M34.76,18.12c-1.11,0-2.28-.35-3.37-1.01-.07-.04-.11-.11-.11-.19,0-.08,.04-.15,.11-.2,.21-.13,.42-.25,.64-.35,.62-.28,1.29-.45,2-.49,.69-.05,1.38,.02,2.05,.21h0c.6,.16,1.17,.42,1.68,.75,.07,.04,.11,.12,.1,.2,0,.08-.04,.15-.11,.19-.72,.43-1.41,.69-2.08,.82-.29,.05-.59,.08-.9,.08Zm-2.8-1.21c1.2,.65,2.47,.89,3.62,.68,.52-.09,1.04-.28,1.6-.57-.38-.21-.79-.38-1.22-.49h0c-.62-.17-1.26-.23-1.9-.19-.66,.04-1.28,.2-1.84,.45-.08,.04-.17,.08-.25,.12Z' />
</g>
<g>
<path
className='cls-1'
d='M30.76,14.09c.33,.14,2.82,.36,3.93,.36,.7,0,3.37-.09,3.97-.25,.32-.08,1.11-.83,1.2-1.04-.18-.1-.38-.16-.61-.16-2.69,.01-6.52-.01-9.07-.03-.12,0-.24,.02-.35,.05,.05,.16,.64,.95,.94,1.08Z'
/>
<path d='M34.66,14.67c-1.06,0-3.6-.2-3.99-.37h0c-.38-.17-1-1.01-1.07-1.22-.02-.06-.01-.12,.02-.18s.08-.09,.14-.11c.13-.04,.28-.05,.41-.06,3.24,.02,6.59,.04,9.07,.03,.26,0,.51,.06,.73,.19,.1,.06,.14,.18,.1,.28-.11,.27-.96,1.08-1.35,1.18-.64,.17-3.39,.26-4.03,.26-.01,0-.02,0-.03,0Zm-4.46-1.48c.19,.26,.5,.62,.65,.68h0c.26,.11,2.67,.34,3.84,.34,.73,0,3.36-.1,3.91-.24,.18-.05,.65-.46,.9-.72-.08-.02-.17-.03-.25-.03-2.48,.01-5.81,0-9.05-.03Z' />
</g>
<path
className='cls-1'
d='M55.06,10.09c-.39,0-.75-.25-.87-.64-.6-1.93-2.15-3.59-4.03-4.33-.47-.18-.7-.72-.52-1.19,.18-.47,.71-.7,1.19-.52,2.43,.95,4.34,3,5.11,5.49,.15,.48-.12,1-.6,1.15-.09,.03-.18,.04-.27,.04Z'
/>
<g>
<path d='M40.98,10.04c0-1.04,.53-1.56,1.18-1.56s1.18,.52,1.18,1.56-.53,1.56-1.18,1.56-1.18-.52-1.18-1.56' />
<path d='M26.28,10.04c0-1.04,.53-1.56,1.18-1.56s1.18,.52,1.18,1.56-.53,1.56-1.18,1.56-1.18-.52-1.18-1.56' />
</g>
<g>
<path
className='cls-2'
d='M24.91,13.48c0,1.16-.94,2.11-2.11,2.11s-2.11-.94-2.11-2.11,.94-2.11,2.11-2.11,2.11,.94,2.11,2.11Z'
/>
<path
className='cls-2'
d='M48.92,13.48c0,1.16-.94,2.11-2.11,2.11s-2.11-.94-2.11-2.11,.94-2.11,2.11-2.11,2.11,.94,2.11,2.11Z'
/>
</g>
<path
className='cls-1'
d='M15.38,20.59c-.33,0-.65-.18-.81-.49-.48-.92-.78-1.96-.85-2.99-.04-.5,.34-.94,.85-.98,.5-.04,.94,.34,.98,.85,.06,.79,.28,1.58,.65,2.28,.23,.45,.06,1-.39,1.23-.14,.07-.28,.1-.42,.1Z'
/>
<path
className='cls-1'
d='M18.01,22.51c-.14,0-.28-.03-.41-.1l-.48-.24c-.45-.23-.64-.78-.41-1.23,.23-.45,.78-.64,1.23-.41l.48,.24c.45,.23,.64,.78,.41,1.23-.16,.32-.48,.51-.82,.51Z'
/>
</g>
</g>
</svg>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function FigThree() {
return <></>
}

View File

@@ -0,0 +1,3 @@
export default function FigTwo() {
return <div>Fig1</div>
}

View File

@@ -0,0 +1,47 @@
'use client'
import {useTranslations} from 'next-intl'
import Image from 'next/image'
import Fig1 from '@/public/images/above/fig-1.svg'
import Fig2 from '@/public/images/above/fig-2.svg'
import Fig3 from '@/public/images/above/fig-3.svg'
export default function Above() {
const t = useTranslations('Banner.Above')
return (
<div className='flex w-full items-end justify-center bg-brand-violet md:min-h-[43px] xl:min-h-[51px]'>
<div className='mx-0 mb-0.5'>
<Image
width={72.79}
height={38.95}
src={Fig1}
alt=''
className='max-h-[35px]'
/>
</div>
<div className='mx-0'>
<Image
width={80.21}
height={43.78}
src={Fig2}
alt=''
className='max-h-[41px]'
/>
</div>
<div className='self-center font-medium text-brand-yellow'>
{t('title')}
</div>
<div className='mx-1'>
<Image
width={98.89}
height={47.27}
src={Fig3}
alt=''
className='max-h-[37px]'
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,175 @@
'use client'
import {
AudioWaveform,
BookOpen,
Bot,
Command,
Frame,
GalleryVerticalEnd,
Map,
PieChart,
Settings2,
SquareTerminal
} from 'lucide-react'
import * as React from 'react'
import {NavMain} from '@/components/shared/nav-main'
import {NavProjects} from '@/components/shared/nav-projects'
import {NavUser} from '@/components/shared/nav-user'
import {TeamSwitcher} from '@/components/shared/team-switcher'
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarRail
} from '@/components/ui/sidebar'
// This is sample data.
const data = {
user: {
name: 'shadcn',
email: 'm@example.com',
avatar: '/avatars/shadcn.jpg'
},
teams: [
{
name: 'Acme Inc',
logo: GalleryVerticalEnd,
plan: 'Enterprise'
},
{
name: 'Acme Corp.',
logo: AudioWaveform,
plan: 'Startup'
},
{
name: 'Evil Corp.',
logo: Command,
plan: 'Free'
}
],
navMain: [
{
title: 'Playground',
url: '#',
icon: SquareTerminal,
isActive: true,
items: [
{
title: 'History',
url: '#'
},
{
title: 'Starred',
url: '#'
},
{
title: 'Settings',
url: '#'
}
]
},
{
title: 'Models',
url: '#',
icon: Bot,
items: [
{
title: 'Genesis',
url: '#'
},
{
title: 'Explorer',
url: '#'
},
{
title: 'Quantum',
url: '#'
}
]
},
{
title: 'Documentation',
url: '#',
icon: BookOpen,
items: [
{
title: 'Introduction',
url: '#'
},
{
title: 'Get Started',
url: '#'
},
{
title: 'Tutorials',
url: '#'
},
{
title: 'Changelog',
url: '#'
}
]
},
{
title: 'Settings',
url: '#',
icon: Settings2,
items: [
{
title: 'General',
url: '#'
},
{
title: 'Team',
url: '#'
},
{
title: 'Billing',
url: '#'
},
{
title: 'Limits',
url: '#'
}
]
}
],
projects: [
{
name: 'Design Engineering',
url: '#',
icon: Frame
},
{
name: 'Sales & Marketing',
url: '#',
icon: PieChart
},
{
name: 'Travel',
url: '#',
icon: Map
}
]
}
export function AppSidebar({...props}: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar {...props}>
<SidebarHeader>
<TeamSwitcher teams={data.teams} />
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavProjects projects={data.projects} />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
}

View File

@@ -0,0 +1,56 @@
'use client'
// https://xdsoft.net/jodit/play.html?spellcheck=true&language=ua&direction=ltr&saveHeightInStorage=true&saveModeInStorage=true&defaultActionOnPaste=insert_as_text&disablePlugins=ai-assistant%2Cmobile%2Cprint%2Cspeech-recognize%2Ctable%2Ctable-keyboard-navigation%2Cpowered-by-jodit%2Ciframe&minHeight=360&maxHeight=NaN&maxWidth=NaN&buttons=bold%2Citalic%2Cunderline%2Cstrikethrough%2Ceraser%2Cul%2Col%2Cfont%2Cfontsize%2Cparagraph%2ClineHeight%2Csuperscript%2Csubscript%2CclassSpan%2Cfile%2Cimage%2Cvideo%2Cspellcheck%2Ccut
//import JoditEditor from 'jodit-react'
import dynamic from 'next/dynamic'
import React, {useMemo, useRef, useState} from 'react'
const JoditEditor = dynamic(() => import('jodit-react'), {ssr: false})
export default function BasicEditor({
placeholder,
maxHeight,
handleChange
}: {
placeholder: string
maxHeight: number
handleChange: any
}) {
const editor = useRef(null)
const [content, setContent] = useState('')
const config = useMemo(
() => ({
readonly: false, // all options from https://xdsoft.net/jodit/docs/,
placeholder: placeholder || 'Start typings...',
spellcheck: true,
language: 'ua',
saveHeightInStorage: true,
saveModeInStorage: true,
//defaultActionOnPaste: 'insert_as_text',
disablePlugins:
'ai-assistant,mobile,print,speech-recognize,table,table-keyboard-navigation,powered-by-jodit,iframe',
minHeight: maxHeight || 320,
maxHeight: 1100,
// maxWidth: 992,
uploader: {
insertImageAsBase64URI: true,
imagesExtensions: ['jpg', 'png', 'jpeg', 'gif', 'svg', 'webp']
},
buttons:
'bold,italic,underline,strikethrough,eraser,ul,ol,font,fontsize,paragraph,lineHeight,superscript,subscript,classSpan,file,image,video,spellcheck,cut'
}),
[placeholder, maxHeight]
)
return (
<JoditEditor
ref={editor}
value={content}
config={config}
tabIndex={1} // tabIndex of textarea
onBlur={newContent => setContent(newContent)} // preferred to use only this option to update the content for performance reasons
onChange={handleChange}
/>
)
}

View File

@@ -0,0 +1,28 @@
'use client'
// 31:47
import {ChevronUp} from 'lucide-react'
import SocialMediaPanel from '@/components/shared/social-media-panel'
import {Button} from '@/ui/button'
export default function Footer() {
return (
<footer className='bg-brand-violet w-full py-3 text-white'>
<div className='container flex items-center justify-between'>
<div>Політика конфіденційності</div>
<div>Договір оферти</div>
<div>Доставка і повернення</div>
<div>Контакти</div>
<SocialMediaPanel color='#fff' />
<Button
variant='ghost'
className='bg-brand-violet rounded-none'
onClick={() => window.scrollTo({top: 0, behavior: 'smooth'})}
>
<ChevronUp className='mr-2 h-4 w-4' />
</Button>
</div>
</footer>
)
}

View File

@@ -0,0 +1,33 @@
import {CircleUserRound} from 'lucide-react'
import Link from 'next/link'
import {auth} from '@/auth'
import {avatarFallback} from '@/lib/utils'
import {Avatar, AvatarFallback, AvatarImage} from '@/ui/avatar'
export default async function CabinetButton() {
const session = await auth()
return (
<Link href='/cabinet' className='header-button' aria-label='Кабінет'>
<button className='flex flex-col items-center' role='button'>
{session ? (
<>
<Avatar className='hs1-[21px] w1-[21px] border2-2 border2-brand-violet'>
<AvatarImage src={session.user?.image as string} alt='avatar' />
<AvatarFallback className='text-xs'>
{avatarFallback(session.user?.name as string)}
</AvatarFallback>
</Avatar>
</>
) : (
<>
<CircleUserRound className='h-[21px] w-[21px]' />
GA4_Ecommerce_View_Item_List_Trigger
</>
)}
{/*<span className='text-sm'>Кабінет</span>*/}
</button>
</Link>
)
}

View File

@@ -0,0 +1,34 @@
import {Heart, ShoppingCartIcon, UserIcon} from 'lucide-react'
import {useTranslations} from 'next-intl'
import CabinetButton from '@/components/shared/header/cabinet-button'
import {Link} from '@/i18n/routing'
export default function HeaderControls() {
const t = useTranslations('cart')
return (
<div className='flex w-full justify-end gap-x-9 text-brand-violet'>
<CabinetButton />
<Link href={'#' as never} className='header-button' aria-label='Вибране'>
<button className='flex flex-col items-center' role='button'>
<Heart className='h-[21px] w-[21px]' />
<span className='font1-bold text-sm'>{t('favorites')}</span>
</button>
</Link>
<Link
href={'/checkout' as never}
className='header-button'
aria-label='Кошик'
>
<button className='flex flex-col items-center' role='button'>
<ShoppingCartIcon className='h-[21px] w-[21px]' />
<span className='font1-bold text-sm'>{t('basket')}</span>
</button>
</Link>
</div>
)
}

View File

@@ -0,0 +1,54 @@
import {Menu} from '@radix-ui/react-menu'
import HeaderControls from './controls'
import Logo from '@/components/shared/home/logo'
import LocaleSwitcher from '@/components/shared/locale-switcher'
import Navbar from '@/components/shared/navbar'
import SearchForm from '@/components/shared/search/form'
import SocialMediaPanel from '@/components/shared/social-media-panel'
import {Switch} from '@/ui/switch'
type MenuProps = {
name: string
slug: string
href: string
}
export default async function Header() {
/*{
searchParams
}: {
searchParams: Promise<{query?: string}>
}*/
//const query = (await searchParams).query
return (
<header className='w-full border-none bg-background text-white'>
<div className='container flex'>
<div className='bw-layout-col-left'>
<Logo />
</div>
<div className='bw-layout-col-right flex-col'>
<div className='mt-1.5 flex h-10 items-center'>
<div className='bw-header-col-left flex justify-between gap-x-10'>
<Navbar />
<SocialMediaPanel size={16} className='gap-x-3 pr-1' />
</div>
<div className='bw-header-col-right flex justify-end text-stone'>
<LocaleSwitcher />
</div>
</div>
<div className='flex items-center'>
<div className='bw-header-col-left'>
<SearchForm />
</div>
<div className='bw-header-col-right'>
<HeaderControls />
</div>
</div>
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,44 @@
import Image from 'next/image'
import * as React from 'react'
import {cards} from '@/lib/data'
import {Card, CardContent} from '@/ui/card'
import {Carousel, CarouselContent, CarouselItem} from '@/ui/carousel'
export default function FeatureCards() {
return (
<Carousel className='mx-auto w-full'>
<CarouselContent className='-ml-2 md:-ml-4'>
{cards.map((card: any) => (
<CarouselItem
key={card.title}
className='pl-3 md:basis-1/3 lg:basis-1/4 xl:basis-1/5'
>
<div className='p-1'>
<Card className='border-[2px] border-brand-violet'>
<CardContent className='aspect-card flex items-center justify-center p-1'>
<CarouselItem>
<Image
src={card.image}
width={256}
height={256}
className='object-scale-down'
priority
alt={''}
/>
</CarouselItem>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
/*<div className='container grid grid-cols-1 md:grid-cols-2 md:gap-44 lg:grid-cols-4'>
{cards.map((card: any) => (
<ProductCard card={card} key={card.title} />
))}
</div>*/
)
}

View File

@@ -0,0 +1,74 @@
'use client'
import Autoplay from 'embla-carousel-autoplay'
import Image from 'next/image'
import Link from 'next/link'
import * as React from 'react'
import {Button} from '@/components/ui/button'
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious
} from '@/components/ui/carousel'
import {cn} from '@/lib/utils'
export function HomeCarousel({
items
}: {
items: {
image: string
url: string
title: string
buttonCaption: string
}[]
}) {
const plugin = React.useRef(Autoplay({delay: 5000, stopOnInteraction: true}))
return (
<Carousel
dir='ltr'
plugins={[plugin.current]}
className='mx-auto w-full'
onMouseEnter={plugin.current.stop}
onMouseLeave={plugin.current.reset}
>
<CarouselContent>
{items.map(item => (
<CarouselItem key={item.title}>
<Link href={item.url}>
<div className='relative -m-0.5 flex aspect-univisium items-center justify-center'>
<Image
src={item.image}
alt={item.title}
fill
className='object-cover'
priority
/>
<div className='absolute left-16 top-1/2 hidden w-1/3 -translate-y-1/2 transform md:left-32'>
<h2
className={cn(
'mb-4 text-lg font-bold text-primary shadow-brand-violet drop-shadow-xl md:text-6xl'
)}
>
{`${item.title}`}
</h2>
<Button className='hidden md:block'>
{`${item.buttonCaption}`}
</Button>
</div>
</div>
</Link>
</CarouselItem>
))}
</CarouselContent>
{/*<CarouselPrevious className='left-0 h-[78px] w-[78px] text-6xl md:left-12' />
<CarouselNext className='right-0 h-[78px] w-[78px] md:right-12' />*/}
<CarouselPrevious className='absolute left-[1rem] top-1/2 z-10 h-[78px] w-[78px] -translate-y-1/2 transform' />
<CarouselNext className='absolute right-[1rem] top-1/2 z-10 h-[78px] w-[78px] -translate-y-1/2 transform' />
</Carousel>
)
}

View File

@@ -0,0 +1,29 @@
'use client'
import Image from 'next/image'
import Link from 'next/link'
import {APP_NAME} from '@/lib/constants'
import logoImg from '@/public/images/logo.svg'
export default function Logo() {
const ar = 121 / 192
const w = 112
return (
<div className='mt-0.5 flex items-center justify-center'>
<Link
href='/'
className='m-1 flex cursor-pointer items-center pt-[7px] text-2xl font-extrabold outline-0'
>
<Image
src={logoImg}
width={w}
height={w * ar}
alt={`${APP_NAME} logo`}
className='w-[131]'
/>
</Link>
</div>
)
}

View File

@@ -0,0 +1,34 @@
import React from 'react'
import IconProps from '@/components/shared/icons/props'
import {cn} from '@/lib/utils'
export default function GoogleIcon({className, size, color}: IconProps) {
return (
<svg
width={size || 24}
height={size || 24}
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
className={cn('bw-icon', className)}
>
<path
d='M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z'
fill='#4285F4'
/>
<path
d='M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z'
fill='#34A853'
/>
<path
d='M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z'
fill='#FBBC05'
/>
<path
d='M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z'
fill='#EA4335'
/>
<path d='M1 1h22v22H1z' fill='none' />
</svg>
)
}

View File

@@ -0,0 +1,5 @@
export default interface IconProps {
className?: string
size?: string | number
color?: string
}

View File

@@ -0,0 +1,22 @@
import IconProps from '@/components/shared/icons/props'
import {BRAND_ICON_COLOR} from '@/lib/constants'
import {cn} from '@/lib/utils'
export default function WhatsappIcon({className, size, color}: IconProps) {
return (
<svg
width={size || 24}
height={size || 24}
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
{/*https://lineicons.com/icons?q=goo*/}
<path
className={cn('bw-icon', className)}
d='M19.074 4.89389C17.2091 3.02894 14.6689 2 12.0644 2C6.59814 2 2.12869 6.4373 2.12869 11.9035C2.12869 13.672 2.57885 15.3441 3.44702 16.8875L2.03223 22L7.33769 20.6495C8.78464 21.4212 10.4245 21.8714 12.0965 21.8714C17.5306 21.8392 21.9679 17.4019 21.9679 11.9035C21.9679 9.26688 20.939 6.791 19.074 4.89389ZM12.0322 20.1672C10.5853 20.1672 9.07403 19.7492 7.82001 18.9775L7.49846 18.7846L4.37949 19.5884L5.24766 16.5659L5.05473 16.2444C4.25088 14.926 3.80072 13.3826 3.80072 11.8392C3.80072 7.30547 7.46631 3.63987 12.0322 3.63987C14.2187 3.63987 16.2766 4.50804 17.82 6.05145C19.3634 7.59486 20.2316 9.68489 20.2316 11.9035C20.2959 16.5016 16.566 20.1672 12.0322 20.1672ZM16.566 13.9936C16.3088 13.865 15.119 13.254 14.8297 13.2219C14.6046 13.1254 14.4116 13.0932 14.283 13.3505C14.1544 13.6077 13.6399 14.1222 13.5113 14.3151C13.3827 14.4437 13.2541 14.508 12.9647 14.3473C12.7075 14.2187 11.9358 13.9936 10.9711 13.0932C10.2316 12.4502 9.71711 11.6463 9.62065 11.3569C9.49203 11.0997 9.5885 11.0032 9.74927 10.8424C9.87788 10.7138 10.0065 10.5852 10.103 10.3923C10.2316 10.2637 10.2316 10.135 10.3602 9.97428C10.4888 9.84566 10.3924 9.65274 10.328 9.52412C10.2316 9.3955 9.78142 8.17364 9.55634 7.65917C9.36342 7.1447 9.13834 7.24116 9.00972 7.24116C8.8811 7.24116 8.68817 7.24116 8.55956 7.24116C8.43094 7.24116 8.1094 7.27331 7.91647 7.5627C7.69139 7.81994 7.0483 8.43087 7.0483 9.65273C7.0483 10.8746 7.91647 12 8.07724 12.2251C8.20586 12.3537 9.84573 14.8939 12.2895 15.9871C12.8682 16.2444 13.3184 16.4051 13.7043 16.5338C14.283 16.7267 14.8297 16.6624 15.2477 16.6302C15.73 16.5981 16.6946 16.0514 16.9197 15.4405C17.1126 14.8939 17.1126 14.3473 17.0483 14.2508C16.984 14.1865 16.7911 14.09 16.566 13.9936Z'
fill={color || BRAND_ICON_COLOR}
/>
</svg>
)
}

View File

@@ -0,0 +1,41 @@
'use client'
import {useLocale} from 'next-intl'
import {redirect} from 'next/navigation'
import {Link, usePathname, useRouter} from '@/i18n/routing'
import {Label} from '@/ui/label'
import {Switch} from '@/ui/switch'
export default function LocaleSwitcher() {
const router = useRouter()
const pathname = usePathname()
const locale = useLocale()
const initialState = locale !== 'uk'
const handler = (state: boolean) => {
const newPath = `/${locale}${pathname}`
//window.history.replaceState(null, '', newPath)
const link = document.getElementById('lang-switch')
if (link) {
link.innerText = `${state ? '/ru' : ''}${pathname}`
link.setAttribute('href', `${state ? '/ru' : ''}${pathname}`)
link.click()
}
}
// router.replace('/cabinet', {locale: checked ? 'ru' : 'uk'}
return (
<div className='flex items-center space-x-2'>
<Link id='lang-switch' href='/' locale='uk'>
LA
</Link>
<Label htmlFor='locale-switcher'>Укр</Label>
<Switch
id='locale-switcher'
defaultChecked={initialState}
onCheckedChange={handler}
/>
<Label htmlFor='locale-switcher'>Рус</Label>
</div>
)
}

View File

@@ -0,0 +1,9 @@
import NavbarMenu from '@/components/shared/navbar/navbar-menu'
export default function Navbar() {
return (
<nav className='text-min flex w-full items-center justify-between text-sm font-medium leading-none'>
<NavbarMenu />
</nav>
)
}

View File

@@ -0,0 +1,44 @@
'use client'
import {Menu as MenuIcon, X} from 'lucide-react'
import Link from 'next/link'
import {useState} from 'react'
import {data} from '@/lib/data'
import {Button} from '@/ui/button'
export default function NavbarMenu() {
const bp = 'md'
const [menuOpened, setMenuOpened] = useState(false)
function ToggleNavbar() {
setMenuOpened(!menuOpened)
}
return (
<>
<div className={`hidden ${bp}:block w-full`}>
<div className='flex items-center justify-between'>
{data.headerMenus.map(item => (
<Link href={item.href + item.slug} className='' key={item.name}>
{item.name}
</Link>
))}
</div>
</div>
<div className={`flex items-center ${bp}:hidden`}>
<Button variant='ghost' onClick={ToggleNavbar}>
{menuOpened ? <X /> : <MenuIcon />}
</Button>
</div>
{menuOpened && (
<div className={`${bp}:hidden`}>
<div className='space-y-1 px-2 pb-3 pt-2'>
<Link href={'#'}>Hidden Menu</Link>
</div>
</div>
)}
</>
)
}

View File

@@ -0,0 +1,33 @@
import {SearchIcon} from 'lucide-react'
import {getTranslations} from 'next-intl/server'
import Form from 'next/form'
import {Button} from '@/ui/button'
import {Input} from '@/ui/input'
export default async function SearchForm({query}: {query?: string}) {
const t = await getTranslations('UI')
return (
<Form
action='/search'
scroll={false}
className='border-stone flex h-10 w-full overflow-hidden rounded-[10px] border-2'
>
<Input
className='h-full flex-1 rounded-none border-0 bg-white text-base text-stone-600 outline-0 dark:border-gray-200'
placeholder={t('search-placeholder')}
name='query'
defaultValue={query}
type='text'
/>
<Button
type='submit'
className='h-full rounded-none border-none bg-neutral-200 px-3 py-2 text-stone-600'
>
{/*<SearchIcon className='size-5' />*/}
{t('search-button')}
</Button>
</Form>
)
}

View File

@@ -0,0 +1,55 @@
'use client'
import {useLocale} from 'next-intl'
import {Link} from '@/i18n/routing'
import {Button} from '@/ui/button'
export default function AppCatalogRender(items: Array<object>) {
const locale = useLocale()
return (
<div className='flex w-full justify-center'>
<div className='bw-dd-menu group inline-block w-full'>
<Button className='py-13 flex h-10 w-full items-center rounded-sm border-none bg-brand-yellow-300 px-3 outline-none focus:outline-none'>
<span className='flex-1 pr-1 font-semibold'>Каталог</span>
<span>
<svg
className='h-4 w-4 transform fill-current transition duration-300 ease-in-out group-hover:-rotate-180'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
>
<path d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z' />
</svg>
</span>
</Button>
<ul className='-bw-app-catalog-collapse mt-2 w-full min-w-32 origin-top transform rounded-sm border bg-white shadow-xl transition duration-300 ease-in-out group-hover:scale-100 hover:shadow-2xl'>
{items?.items.map((item: any) => (
<li
className='cursor-pointer rounded-none py-2.5 pl-3 pr-1.5 text-sm font-medium hover:bg-[#442d88]/10 xl:py-3'
key={item.id}
>
<button className='flex w-full items-center text-left outline-none focus:outline-none'>
<Link
className='flex-1 pr-1 leading-none xl:leading-[1.3]'
href={`/category/${item.id}-${item.locales[locale === 'uk' ? 0 : 1].slug}`}
>
{item.locales[locale === 'uk' ? 0 : 1].title}
</Link>
<span className='mr-auto'>
<svg
className='h-4 w-4 fill-current transition duration-150 ease-in-out'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
>
<path d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z' />
</svg>
</span>
</button>
</li>
))}
</ul>
</div>
</div>
)
}

View File

@@ -0,0 +1,16 @@
'use server'
import AppCatalogRender from '@/components/shared/sidebar/app-catalog-render'
import {db} from '@/lib/db/prisma/client'
const appCatalog = async () => {
return db.category.findMany({
include: {
locales: true
}
})
}
export default async function AppCatalog() {
return <AppCatalogRender items={await appCatalog()} />
}

View File

@@ -0,0 +1,82 @@
import IconProps from '@/components/shared/icons/props'
import WhatsappIcon from '@/components/shared/icons/whatsapp'
import {BRAND_ICON_COLOR, BRAND_ICON_SIZE} from '@/lib/constants'
import {cn} from '@/lib/utils'
export default function SocialMediaPanel({className, size, color}: IconProps) {
return (
<aside className={cn('flex items-center gap-x-4', className)}>
{/*facebook*/}
<div>
<svg
width={size || BRAND_ICON_SIZE}
height={size || BRAND_ICON_SIZE * 1.041666666666667}
viewBox='0 0 24 25'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M12 2.53906C17.5229 2.53906 22 7.01621 22 12.5391C22 17.5304 18.3431 21.6674 13.5625 22.4176V15.4297H15.8926L16.3359 12.5391L13.5625 12.5387V10.6632C13.5625 10.657 13.5625 10.6509 13.5626 10.6447C13.5626 10.6354 13.5628 10.6262 13.5629 10.6169C13.578 9.84259 13.9742 9.10156 15.1921 9.10156H16.4531V6.64062C16.4531 6.64062 15.3087 6.44492 14.2146 6.44492C11.966 6.44492 10.4842 7.78652 10.4386 10.2193C10.4379 10.2578 10.4375 10.2965 10.4375 10.3355V12.5387H7.89844V15.4293L10.4375 15.4297V22.4172C5.65686 21.667 2 17.5304 2 12.5391C2 7.01621 6.47715 2.53906 12 2.53906Z'
fill={color || BRAND_ICON_COLOR}
/>
</svg>
</div>
<div>
<WhatsappIcon size={size} color={color} />
</div>
{/*Telegram*/}
<div>
<svg
width={size || BRAND_ICON_SIZE}
height={size || BRAND_ICON_SIZE}
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M21.936 5.17077L18.9059 19.3546C18.6802 20.3539 18.1 20.5795 17.2618 20.1282L12.7166 16.7757L10.4923 18.9033C10.2666 19.1289 10.041 19.3546 9.5252 19.3546L9.8798 14.6804L18.3578 6.97598C18.7124 6.62138 18.2611 6.49244 17.8098 6.78256L7.26869 13.4232L2.72343 12.037C1.72412 11.7147 1.72412 11.0377 2.94908 10.5864L20.6144 3.72015C21.4847 3.46227 22.2262 3.91357 21.936 5.17077Z'
fill={color || BRAND_ICON_COLOR}
/>
</svg>
</div>
{/*Instagram*/}
<div>
<svg
width={size || BRAND_ICON_SIZE}
height={size || BRAND_ICON_SIZE}
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M8.6672 12C8.6672 10.1591 10.1591 8.6664 12 8.6664C13.8409 8.6664 15.3336 10.1591 15.3336 12C15.3336 13.8409 13.8409 15.3336 12 15.3336C10.1591 15.3336 8.6672 13.8409 8.6672 12ZM6.86512 12C6.86512 14.836 9.164 17.1349 12 17.1349C14.836 17.1349 17.1349 14.836 17.1349 12C17.1349 9.164 14.836 6.86512 12 6.86512C9.164 6.86512 6.86512 9.164 6.86512 12ZM16.1382 6.66152C16.1381 6.89886 16.2084 7.13089 16.3401 7.32829C16.4719 7.52568 16.6593 7.67956 16.8785 7.77047C17.0977 7.86138 17.339 7.88525 17.5718 7.83904C17.8046 7.79283 18.0185 7.67862 18.1863 7.51087C18.3542 7.34311 18.4686 7.12934 18.515 6.89658C18.5614 6.66382 18.5377 6.42253 18.447 6.20322C18.3563 5.98392 18.2025 5.79644 18.0052 5.6645C17.808 5.53257 17.576 5.4621 17.3386 5.462H17.3382C17.02 5.46215 16.715 5.58856 16.49 5.81347C16.265 6.03837 16.1384 6.34339 16.1382 6.66152ZM7.96 20.1398C6.98504 20.0954 6.45512 19.933 6.10296 19.7958C5.63608 19.614 5.30296 19.3975 4.95272 19.0478C4.60248 18.698 4.38568 18.3652 4.20472 17.8983C4.06744 17.5463 3.90504 17.0162 3.86072 16.0413C3.81224 14.9872 3.80256 14.6706 3.80256 12.0001C3.80256 9.3296 3.81304 9.01384 3.86072 7.95888C3.90512 6.98392 4.06872 6.45488 4.20472 6.10184C4.38648 5.63496 4.60296 5.30184 4.95272 4.9516C5.30248 4.60136 5.63528 4.38456 6.10296 4.2036C6.45496 4.06632 6.98504 3.90392 7.96 3.8596C9.01408 3.81112 9.33072 3.80144 12 3.80144C14.6693 3.80144 14.9862 3.81192 16.0412 3.8596C17.0162 3.904 17.5452 4.0676 17.8982 4.2036C18.3651 4.38456 18.6982 4.60184 19.0485 4.9516C19.3987 5.30136 19.6147 5.63496 19.7965 6.10184C19.9338 6.45384 20.0962 6.98392 20.1405 7.95888C20.189 9.01384 20.1986 9.3296 20.1986 12.0001C20.1986 14.6706 20.189 14.9863 20.1405 16.0413C20.0961 17.0162 19.9329 17.5462 19.7965 17.8983C19.6147 18.3652 19.3982 18.6983 19.0485 19.0478C18.6987 19.3972 18.3651 19.614 17.8982 19.7958C17.5462 19.933 17.0162 20.0954 16.0412 20.1398C14.9871 20.1882 14.6705 20.1979 12 20.1979C9.32952 20.1979 9.01376 20.1882 7.96 20.1398ZM7.8772 2.06056C6.81264 2.10904 6.0852 2.27784 5.44992 2.52504C4.792 2.78032 4.23504 3.1228 3.67848 3.67848C3.12192 4.23416 2.78032 4.792 2.52504 5.44992C2.27784 6.0856 2.10904 6.81264 2.06056 7.8772C2.01128 8.94344 2 9.28432 2 12C2 14.7157 2.01128 15.0566 2.06056 16.1228C2.10904 17.1874 2.27784 17.9144 2.52504 18.5501C2.78032 19.2076 3.122 19.7661 3.67848 20.3215C4.23496 20.877 4.792 21.219 5.44992 21.475C6.0864 21.7222 6.81264 21.891 7.8772 21.9394C8.944 21.9879 9.28432 22 12 22C14.7157 22 15.0566 21.9887 16.1228 21.9394C17.1874 21.891 17.9144 21.7222 18.5501 21.475C19.2076 21.219 19.765 20.8772 20.3215 20.3215C20.8781 19.7658 21.219 19.2076 21.475 18.5501C21.7222 17.9144 21.8918 17.1874 21.9394 16.1228C21.9879 15.0558 21.9992 14.7157 21.9992 12C21.9992 9.28432 21.9879 8.94344 21.9394 7.8772C21.891 6.81256 21.7222 6.0852 21.475 5.44992C21.219 4.7924 20.8772 4.23504 20.3215 3.67848C19.7658 3.12192 19.2076 2.78032 18.5509 2.52504C17.9144 2.27784 17.1874 2.10824 16.1236 2.06056C15.0574 2.01208 14.7165 2 12.0008 2C9.28512 2 8.944 2.01128 7.8772 2.06056Z'
fill={color || BRAND_ICON_COLOR}
/>
<path
d='M8.6672 12C8.6672 10.1591 10.1591 8.6664 12 8.6664C13.8409 8.6664 15.3336 10.1591 15.3336 12C15.3336 13.8409 13.8409 15.3336 12 15.3336C10.1591 15.3336 8.6672 13.8409 8.6672 12ZM6.86512 12C6.86512 14.836 9.164 17.1349 12 17.1349C14.836 17.1349 17.1349 14.836 17.1349 12C17.1349 9.164 14.836 6.86512 12 6.86512C9.164 6.86512 6.86512 9.164 6.86512 12ZM16.1382 6.66152C16.1381 6.89886 16.2084 7.13089 16.3401 7.32829C16.4719 7.52568 16.6593 7.67956 16.8785 7.77047C17.0977 7.86138 17.339 7.88525 17.5718 7.83904C17.8046 7.79283 18.0185 7.67862 18.1863 7.51087C18.3542 7.34311 18.4686 7.12934 18.515 6.89658C18.5614 6.66382 18.5377 6.42253 18.447 6.20322C18.3563 5.98392 18.2025 5.79644 18.0052 5.6645C17.808 5.53257 17.576 5.4621 17.3386 5.462H17.3382C17.02 5.46215 16.715 5.58856 16.49 5.81347C16.265 6.03837 16.1384 6.34339 16.1382 6.66152ZM7.96 20.1398C6.98504 20.0954 6.45512 19.933 6.10296 19.7958C5.63608 19.614 5.30296 19.3975 4.95272 19.0478C4.60248 18.698 4.38568 18.3652 4.20472 17.8983C4.06744 17.5463 3.90504 17.0162 3.86072 16.0413C3.81224 14.9872 3.80256 14.6706 3.80256 12.0001C3.80256 9.3296 3.81304 9.01384 3.86072 7.95888C3.90512 6.98392 4.06872 6.45488 4.20472 6.10184C4.38648 5.63496 4.60296 5.30184 4.95272 4.9516C5.30248 4.60136 5.63528 4.38456 6.10296 4.2036C6.45496 4.06632 6.98504 3.90392 7.96 3.8596C9.01408 3.81112 9.33072 3.80144 12 3.80144C14.6693 3.80144 14.9862 3.81192 16.0412 3.8596C17.0162 3.904 17.5452 4.0676 17.8982 4.2036C18.3651 4.38456 18.6982 4.60184 19.0485 4.9516C19.3987 5.30136 19.6147 5.63496 19.7965 6.10184C19.9338 6.45384 20.0962 6.98392 20.1405 7.95888C20.189 9.01384 20.1986 9.3296 20.1986 12.0001C20.1986 14.6706 20.189 14.9863 20.1405 16.0413C20.0961 17.0162 19.9329 17.5462 19.7965 17.8983C19.6147 18.3652 19.3982 18.6983 19.0485 19.0478C18.6987 19.3972 18.3651 19.614 17.8982 19.7958C17.5462 19.933 17.0162 20.0954 16.0412 20.1398C14.9871 20.1882 14.6705 20.1979 12 20.1979C9.32952 20.1979 9.01376 20.1882 7.96 20.1398ZM7.8772 2.06056C6.81264 2.10904 6.0852 2.27784 5.44992 2.52504C4.792 2.78032 4.23504 3.1228 3.67848 3.67848C3.12192 4.23416 2.78032 4.792 2.52504 5.44992C2.27784 6.0856 2.10904 6.81264 2.06056 7.8772C2.01128 8.94344 2 9.28432 2 12C2 14.7157 2.01128 15.0566 2.06056 16.1228C2.10904 17.1874 2.27784 17.9144 2.52504 18.5501C2.78032 19.2076 3.122 19.7661 3.67848 20.3215C4.23496 20.877 4.792 21.219 5.44992 21.475C6.0864 21.7222 6.81264 21.891 7.8772 21.9394C8.944 21.9879 9.28432 22 12 22C14.7157 22 15.0566 21.9887 16.1228 21.9394C17.1874 21.891 17.9144 21.7222 18.5501 21.475C19.2076 21.219 19.765 20.8772 20.3215 20.3215C20.8781 19.7658 21.219 19.2076 21.475 18.5501C21.7222 17.9144 21.8918 17.1874 21.9394 16.1228C21.9879 15.0558 21.9992 14.7157 21.9992 12C21.9992 9.28432 21.9879 8.94344 21.9394 7.8772C21.891 6.81256 21.7222 6.0852 21.475 5.44992C21.219 4.7924 20.8772 4.23504 20.3215 3.67848C19.7658 3.12192 19.2076 2.78032 18.5509 2.52504C17.9144 2.27784 17.1874 2.10824 16.1236 2.06056C15.0574 2.01208 14.7165 2 12.0008 2C9.28512 2 8.944 2.01128 7.8772 2.06056Z'
fill={color || BRAND_ICON_COLOR}
/>
</svg>
</div>
{/*Youtube*/}
<div>
<svg
width={((size || BRAND_ICON_SIZE) as number) * 1.5}
height={((size || BRAND_ICON_SIZE) as number) * 1.5}
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M21.5806 7.19355C21.3548 6.32258 20.6774 5.64516 19.8065 5.41935C18.2581 5 12 5 12 5C12 5 5.74194 5 4.19355 5.41935C3.32258 5.64516 2.64516 6.32258 2.41935 7.19355C2 8.77419 2 12 2 12C2 12 2 15.2581 2.41935 16.8065C2.64516 17.6774 3.32258 18.3548 4.19355 18.5806C5.74194 19 12 19 12 19C12 19 18.2581 19 19.8065 18.5806C20.6774 18.3548 21.3548 17.6774 21.5806 16.8065C22 15.2581 22 12 22 12C22 12 22 8.77419 21.5806 7.19355ZM10 15V9L15.1935 12L10 15Z'
fill={color || BRAND_ICON_COLOR}
/>
</svg>
</div>
</aside>
)
}

View File

@@ -0,0 +1,30 @@
import Image from 'next/image'
import Link from 'next/link'
import {Card, CardContent, CardFooter} from '@/ui/card'
export type CardItem = {
title: string
image: string
href: string
price: number
}
export default function ProductCard({card}: {card: CardItem}) {
return (
<Card key={card.title} className='flex flex-col'>
<Link key={card.title} href={card.href} className='flex flex-col'>
<CardContent className='flex-1 p-4'>
<Image
src={card.image}
alt={card.title}
width={120}
height={120}
className='aspect-card mx-auto h-auto max-w-full object-cover'
/>
</CardContent>
<CardFooter>{card.title}</CardFooter>
</Link>
</Card>
)
}

View File

@@ -0,0 +1,31 @@
'use client'
import {useLocale} from 'next-intl'
import {Link} from '@/i18n/routing'
export default function TempComponent(data: object[]) {
const locale = useLocale()
/*console.log(data)*/
const items = data?.data as object[]
return (
<>
<code>{locale}</code>
{/*<pre>{JSON.stringify(data, null, 2)}</pre>*/}
<pre>
{items.map((item: any) => (
<div key={item.id}>
<Link
href={`/category/${item.id}-${item.locales[locale === 'uk' ? 0 : 1].slug}`}
>
{item.locales[locale === 'uk' ? 0 : 1].title}
</Link>
</div>
))}
</pre>
</>
)
}

50
components/ui/avatar.tsx Normal file
View File

@@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

59
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,59 @@
import {Slot} from '@radix-ui/react-slot'
import {type VariantProps, cva} from 'class-variance-authority'
import * as React from 'react'
import {cn} from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
brand:
'bg-brand-yellow text-primary-foreground shadow hover:bg-primary/90',
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({className, variant, size, asChild = false, ...props}, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={cn(buttonVariants({variant, size, className}))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = 'Button'
export {Button, buttonVariants}

76
components/ui/card.tsx Normal file
View File

@@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

266
components/ui/carousel.tsx Normal file
View File

@@ -0,0 +1,266 @@
'use client'
import useEmblaCarousel, {type UseEmblaCarouselType} from 'embla-carousel-react'
import {ArrowLeft, ArrowRight, ChevronLeft, ChevronRight} from 'lucide-react'
import * as React from 'react'
import {Button} from '@/components/ui/button'
import {cn} from '@/lib/utils'
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: 'horizontal' | 'vertical'
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error('useCarousel must be used within a <Carousel />')
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = 'horizontal',
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === 'horizontal' ? 'x' : 'y'
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'ArrowLeft') {
event.preventDefault()
scrollPrev()
} else if (event.key === 'ArrowRight') {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on('reInit', onSelect)
api.on('select', onSelect)
return () => {
api?.off('select', onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn('relative', className)}
role='region'
aria-roledescription='carousel'
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = 'Carousel'
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({className, ...props}, ref) => {
const {carouselRef, orientation} = useCarousel()
return (
<div ref={carouselRef} className='overflow-hidden'>
<div
ref={ref}
className={cn(
'flex',
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = 'CarouselContent'
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({className, ...props}, ref) => {
const {orientation} = useCarousel()
return (
<div
ref={ref}
role='group'
aria-roledescription='slide'
className={cn(
'min-w-0 shrink-0 grow-0 basis-full',
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
className
)}
{...props}
/>
)
})
CarouselItem.displayName = 'CarouselItem'
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({className, variant = 'outline', size = 'icon', ...props}, ref) => {
const {orientation, scrollPrev, canScrollPrev} = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
'absolute h-8 w-8 rounded-full',
orientation === 'horizontal'
? '-left-12 top-1/2 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
{/*<ArrowLeft className='h-4 w-4' />*/}
<ChevronLeft
size={72}
className='h-[78px] w-[78px]'
absoluteStrokeWidth
/>
<span className='sr-only'>Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = 'CarouselPrevious'
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({className, variant = 'outline', size = 'icon', ...props}, ref) => {
const {orientation, scrollNext, canScrollNext} = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
'absolute h-8 w-8 rounded-full',
orientation === 'horizontal'
? '-right-12 top-1/2 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
{/*<ArrowRight className='h-4 w-4' />*/}
<ChevronRight size={48} absoluteStrokeWidth />
<span className='sr-only'>Next slide</span>
</Button>
)
})
CarouselNext.displayName = 'CarouselNext'
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext
}

View File

@@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

122
components/ui/dialog.tsx Normal file
View File

@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -0,0 +1,201 @@
'use client'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import {Check, ChevronRight, Circle} from 'lucide-react'
import * as React from 'react'
import {cn} from '@/lib/utils'
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({className, inset, children, ...props}, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open1]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className='ml-auto' />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({className, ...props}, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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',
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({className, sideOffset = 4, ...props}, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({className, inset, ...props}, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({className, children, checked, ...props}, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
{...props}
>
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator>
<Check className='h-4 w-4' />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({className, children, ...props}, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator>
<Circle className='h-2 w-2 fill-current' />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({className, inset, ...props}, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({className, ...props}, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup
}

178
components/ui/form.tsx Normal file
View File

@@ -0,0 +1,178 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

23
components/ui/input.tsx Normal file
View File

@@ -0,0 +1,23 @@
import * as React from 'react'
import {cn} from '@/lib/utils'
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
({className, type, ...props}, ref) => {
return (
<input
type={type}
className={cn(
/*focus-visible:ring-1 focus-visible:ring-ring*/
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export {Input}

26
components/ui/label.tsx Normal file
View File

@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

159
components/ui/select.tsx Normal file
View File

@@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

140
components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,140 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

763
components/ui/sidebar.tsx Normal file
View File

@@ -0,0 +1,763 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority"
import { PanelLeft } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
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 } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar:state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContext = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContext | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
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 contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
className
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
}
)
SidebarProvider.displayName = "SidebarProvider"
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}
>(
(
{
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
},
ref
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
className
)}
ref={ref}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
ref={ref}
className="group peer hidden md:block text-sidebar-foreground"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
)}
/>
<div
className={cn(
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]: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)_+_theme(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}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
)
}
)
Sidebar.displayName = "Sidebar"
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
})
SidebarTrigger.displayName = "SidebarTrigger"
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button">
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
})
SidebarRail.displayName = "SidebarRail"
const SidebarInset = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"main">
>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className
)}
{...props}
/>
)
})
SidebarInset.displayName = "SidebarInset"
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className
)}
{...props}
/>
)
})
SidebarInput.displayName = "SidebarInput"
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarHeader.displayName = "SidebarHeader"
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarFooter.displayName = "SidebarFooter"
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
)
})
SidebarSeparator.displayName = "SidebarSeparator"
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
})
SidebarContent.displayName = "SidebarContent"
const SidebarGroup = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
})
SidebarGroup.displayName = "SidebarGroup"
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div"
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
})
SidebarGroupLabel.displayName = "SidebarGroupLabel"
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarGroupAction.displayName = "SidebarGroupAction"
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
))
SidebarGroupContent.displayName = "SidebarGroupContent"
const SidebarMenu = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
))
SidebarMenu.displayName = "SidebarMenu"
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
))
SidebarMenuItem.displayName = "SidebarMenuItem"
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>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 shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
)
SidebarMenuButton.displayName = "SidebarMenuButton"
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className
)}
{...props}
/>
)
})
SidebarMenuAction.displayName = "SidebarMenuAction"
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuBadge.displayName = "SidebarMenuBadge"
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 flex-1 max-w-[--skeleton-width]"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
})
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuSub.displayName = "SidebarMenuSub"
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ ...props }, ref) => <li ref={ref} {...props} />)
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground 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 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}

View File

@@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}
export { Skeleton }

29
components/ui/switch.tsx Normal file
View File

@@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

55
components/ui/tabs.tsx Normal file
View File

@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

135
components/ui/toast.tsx Normal file
View File

@@ -0,0 +1,135 @@
'use client'
import * as ToastPrimitives from '@radix-ui/react-toast'
import {type VariantProps, cva} from 'class-variance-authority'
import {X} from 'lucide-react'
import * as React from 'react'
import {cn} from '@/lib/utils'
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({className, ...props}, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{
variants: {
variant: {
default: 'border bg-background text-foreground',
destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground',
success:
'group text-emerald-950-foreground border-emerald-600 bg-emerald-200',
warning:
'group text-amber-950-foreground border-amber-700 bg-amber-200',
brand:
'text-brand-violet-950 border-brand-violet-200 bg-brand-violet-50'
}
},
defaultVariants: {
variant: 'brand'
}
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({className, variant, ...props}, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({variant}), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({className, ...props}, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors group-[.destructive]:border-muted/40 hover:bg-secondary group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground focus:outline-none focus:ring-1 focus:ring-ring group-[.destructive]:focus:ring-destructive disabled:pointer-events-none disabled:opacity-50',
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({className, ...props}, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity group-hover:opacity-100 group-[.destructive]:text-red-300 hover:text-foreground group-[.destructive]:hover:text-red-50 focus:opacity-100 focus:outline-none focus:ring-1 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className
)}
toast-close=''
{...props}
>
<X className='h-4 w-4' />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({className, ...props}, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold [&+div]:text-xs', className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({className, ...props}, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction
}

35
components/ui/toaster.tsx Normal file
View File

@@ -0,0 +1,35 @@
"use client"
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

32
components/ui/tooltip.tsx Normal file
View File

@@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=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",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }