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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user