added tons of features
This commit is contained in:
26
components/(protected)/admin/auth/permission.tsx
Normal file
26
components/(protected)/admin/auth/permission.tsx
Normal 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()
|
||||
}
|
||||
}
|
||||
171
components/(protected)/admin/category/create-form.tsx
Normal file
171
components/(protected)/admin/category/create-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
47
components/(protected)/admin/footer.tsx
Normal file
47
components/(protected)/admin/footer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
41
components/(protected)/admin/header.tsx
Normal file
41
components/(protected)/admin/header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
442
components/(protected)/admin/product/create-edit-form.tsx
Normal file
442
components/(protected)/admin/product/create-edit-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
169
components/(protected)/admin/product/create-form.tsx.back
Normal file
169
components/(protected)/admin/product/create-form.tsx.back
Normal 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>
|
||||
// )
|
||||
}
|
||||
106
components/(protected)/admin/sidebar.tsx
Normal file
106
components/(protected)/admin/sidebar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
18
components/auth/auth-header.tsx
Normal file
18
components/auth/auth-header.tsx
Normal 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
|
||||
16
components/auth/back-button.tsx
Normal file
16
components/auth/back-button.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
37
components/auth/card-wrapper.tsx
Normal file
37
components/auth/card-wrapper.tsx
Normal 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
|
||||
17
components/auth/form-error.tsx
Normal file
17
components/auth/form-error.tsx
Normal 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
|
||||
15
components/auth/form-success.tsx
Normal file
15
components/auth/form-success.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
104
components/auth/forms/login-form.tsx
Normal file
104
components/auth/forms/login-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
133
components/auth/forms/register-form.tsx
Normal file
133
components/auth/forms/register-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
14
components/auth/forms/sign-out-button.tsx
Normal file
14
components/auth/forms/sign-out-button.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
components/auth/google-login.tsx
Normal file
29
components/auth/google-login.tsx
Normal 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
|
||||
60
components/cabinet/index.tsx
Normal file
60
components/cabinet/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
19
components/shared/above/css/fig-one.module.css
Normal file
19
components/shared/above/css/fig-one.module.css
Normal 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;
|
||||
}
|
||||
115
components/shared/above/fig-one.tsx
Normal file
115
components/shared/above/fig-one.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
3
components/shared/above/fig-three.tsx
Normal file
3
components/shared/above/fig-three.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function FigThree() {
|
||||
return <></>
|
||||
}
|
||||
3
components/shared/above/fig-two.tsx
Normal file
3
components/shared/above/fig-two.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function FigTwo() {
|
||||
return <div>Fig1</div>
|
||||
}
|
||||
47
components/shared/above/index.tsx
Normal file
47
components/shared/above/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
175
components/shared/app-sidebar.tsx~
Normal file
175
components/shared/app-sidebar.tsx~
Normal 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>
|
||||
)
|
||||
}
|
||||
56
components/shared/editor/jodit.tsx
Normal file
56
components/shared/editor/jodit.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
28
components/shared/footer.tsx
Normal file
28
components/shared/footer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
33
components/shared/header/cabinet-button.tsx
Normal file
33
components/shared/header/cabinet-button.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
34
components/shared/header/controls.tsx
Normal file
34
components/shared/header/controls.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
54
components/shared/header/index.tsx
Normal file
54
components/shared/header/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
44
components/shared/home/feature-cards.tsx
Normal file
44
components/shared/home/feature-cards.tsx
Normal 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>*/
|
||||
)
|
||||
}
|
||||
74
components/shared/home/home-carousel.tsx
Normal file
74
components/shared/home/home-carousel.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
components/shared/home/logo.tsx
Normal file
29
components/shared/home/logo.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
34
components/shared/icons/google.tsx
Normal file
34
components/shared/icons/google.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
5
components/shared/icons/props.ts
Normal file
5
components/shared/icons/props.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default interface IconProps {
|
||||
className?: string
|
||||
size?: string | number
|
||||
color?: string
|
||||
}
|
||||
22
components/shared/icons/whatsapp.tsx
Normal file
22
components/shared/icons/whatsapp.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
41
components/shared/locale-switcher.tsx
Normal file
41
components/shared/locale-switcher.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
9
components/shared/navbar/index.tsx
Normal file
9
components/shared/navbar/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
44
components/shared/navbar/navbar-menu.tsx
Normal file
44
components/shared/navbar/navbar-menu.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
33
components/shared/search/form.tsx
Normal file
33
components/shared/search/form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
55
components/shared/sidebar/app-catalog-render.tsx
Normal file
55
components/shared/sidebar/app-catalog-render.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
16
components/shared/sidebar/app-catalog.tsx
Normal file
16
components/shared/sidebar/app-catalog.tsx
Normal 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()} />
|
||||
}
|
||||
82
components/shared/social-media-panel.tsx
Normal file
82
components/shared/social-media-panel.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
30
components/shared/store/product-card.tsx
Normal file
30
components/shared/store/product-card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
31
components/temp-component.tsx
Normal file
31
components/temp-component.tsx
Normal 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
50
components/ui/avatar.tsx
Normal 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 }
|
||||
115
components/ui/breadcrumb.tsx
Normal file
115
components/ui/breadcrumb.tsx
Normal 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
59
components/ui/button.tsx
Normal 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
76
components/ui/card.tsx
Normal 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
266
components/ui/carousel.tsx
Normal 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
|
||||
}
|
||||
11
components/ui/collapsible.tsx
Normal file
11
components/ui/collapsible.tsx
Normal 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
122
components/ui/dialog.tsx
Normal 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,
|
||||
}
|
||||
201
components/ui/dropdown-menu.tsx
Normal file
201
components/ui/dropdown-menu.tsx
Normal 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
178
components/ui/form.tsx
Normal 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
23
components/ui/input.tsx
Normal 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
26
components/ui/label.tsx
Normal 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
159
components/ui/select.tsx
Normal 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,
|
||||
}
|
||||
31
components/ui/separator.tsx
Normal file
31
components/ui/separator.tsx
Normal 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
140
components/ui/sheet.tsx
Normal 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
763
components/ui/sidebar.tsx
Normal 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,
|
||||
}
|
||||
15
components/ui/skeleton.tsx
Normal file
15
components/ui/skeleton.tsx
Normal 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
29
components/ui/switch.tsx
Normal 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
55
components/ui/tabs.tsx
Normal 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
135
components/ui/toast.tsx
Normal 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
35
components/ui/toaster.tsx
Normal 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
32
components/ui/tooltip.tsx
Normal 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 }
|
||||
Reference in New Issue
Block a user