stuff done
This commit is contained in:
347
components/(protected)/admin/entity/crud-form.tsx
Normal file
347
components/(protected)/admin/entity/crud-form.tsx
Normal file
@@ -0,0 +1,347 @@
|
||||
'use client'
|
||||
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import {EntityType} from '@prisma/client'
|
||||
import dynamic from 'next/dynamic'
|
||||
import React, {Suspense, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {useFieldArray, useForm} from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
import {z} from 'zod'
|
||||
|
||||
import {onEntityCreateEditAction} from '@/actions/admin/entity'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select'
|
||||
import {i18nDefaultLocale, i18nLocales} from '@/i18n-config'
|
||||
import {BaseEditorConfig} from '@/lib/config/editor'
|
||||
import {
|
||||
EntityTypeDescription,
|
||||
createEntityFormSchema
|
||||
} from '@/lib/schemas/admin/entity'
|
||||
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 = {
|
||||
type: '',
|
||||
media: '',
|
||||
title: '',
|
||||
annotation: '',
|
||||
body: ''
|
||||
}
|
||||
|
||||
let metaValues = {
|
||||
title: '',
|
||||
description: '',
|
||||
keywords: '',
|
||||
author: ''
|
||||
}
|
||||
|
||||
export const EntityCrudForm = ({data}: {data?: any}) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
const [annotation0, setAnnotation0] = useState(
|
||||
data?.locales[0].annotation || ''
|
||||
)
|
||||
const [annotation1, setAnnotation1] = useState(
|
||||
data?.locales[1].annotation || ''
|
||||
)
|
||||
const [body0, setBody0] = useState(data?.locales[0].body || '')
|
||||
const [body1, setBody1] = useState(data?.locales[1].body || '')
|
||||
|
||||
const editor = useRef(null) //declared a null value
|
||||
const config = useMemo(() => BaseEditorConfig, [])
|
||||
config.maxWidth = '100%'
|
||||
|
||||
const form = useForm<z.infer<typeof createEntityFormSchema>>({
|
||||
resolver: zodResolver(createEntityFormSchema),
|
||||
mode: 'onBlur',
|
||||
defaultValues: data
|
||||
? (data => {
|
||||
const {locales, meta} = data
|
||||
|
||||
return {
|
||||
published: data.published,
|
||||
image: data.image,
|
||||
locales: toEmptyParams(locales) as any,
|
||||
meta: meta
|
||||
? (toEmptyParams(meta) as any)
|
||||
: [{...metaValues}, {...metaValues}]
|
||||
}
|
||||
})(data)
|
||||
: {
|
||||
scopes: '',
|
||||
published: false,
|
||||
media: '',
|
||||
slug: '',
|
||||
locales: [
|
||||
{lang: 'uk', ...localesValues},
|
||||
{lang: 'ru', ...localesValues}
|
||||
],
|
||||
meta: [{...metaValues}, {...metaValues}]
|
||||
}
|
||||
})
|
||||
|
||||
const {register, setValue} = form
|
||||
|
||||
useEffect(() => {
|
||||
register('locales.0.annotation')
|
||||
register('locales.1.annotation')
|
||||
register('locales.0.body')
|
||||
register('locales.1.body')
|
||||
}, [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 createEntityFormSchema>) => {
|
||||
setLoading(true)
|
||||
onEntityCreateEditAction(values).then((res: any) => {
|
||||
if (res?.error) {
|
||||
setError(res?.error)
|
||||
setSuccess('')
|
||||
setLoading(false)
|
||||
toast.error(res?.error)
|
||||
} else {
|
||||
setSuccess(res?.success as string)
|
||||
setError('')
|
||||
setLoading(false)
|
||||
toast.success(res?.success)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
action=''
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='form-horizontal'
|
||||
>
|
||||
<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>
|
||||
<div className='flex flex-row items-center justify-between gap-4 rounded-lg border bg-gray-50/25 px-4 pb-4'>
|
||||
<div className='w-1/3'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='type'
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Тип сутності</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Потрібно обрати тип сутності' />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{Object.keys(EntityType).map(
|
||||
(entityType: string, index: number) => (
|
||||
<SelectItem key={index} value={entityType}>
|
||||
{EntityTypeDescription[entityType] || entityType}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage className='ml-3' />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='w-1/3'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='slug'
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Аліас / Slug</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='вказіть аліас ресурсу'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className='ml-3' />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='w-1/3'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='scopes'
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Область виведення</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='Веддіть дані у JSON форматі'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className='ml-3' />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row items-center justify-between gap-4 rounded-lg border bg-gray-50 px-4 pb-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='media'
|
||||
render={({field}) => (
|
||||
<FormItem className='w-full'>
|
||||
<FormLabel>
|
||||
Медіа (файл на диску чи URL посилання на ресурс)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='text' placeholder='' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage className='ml-3' />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Tabs
|
||||
defaultValue={i18nDefaultLocale}
|
||||
className='mt-4 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'}>
|
||||
<FormControl>
|
||||
<Input type='text' placeholder='' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className='w-full'>
|
||||
<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-full'>
|
||||
<FormLabel>Анотація / Коротка назва</FormLabel>
|
||||
<JoditEditor
|
||||
key={index + 4}
|
||||
ref={editor}
|
||||
config={config}
|
||||
value={index === 0 ? annotation0 : annotation1}
|
||||
className='mt-4 w-full'
|
||||
onBlur={value => {
|
||||
index === 0 ? setAnnotation0(value) : setAnnotation1(value)
|
||||
setValue(`locales.${index}.annotation`, value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<FormLabel>Текст</FormLabel>
|
||||
<JoditEditor
|
||||
key={index + 4}
|
||||
ref={editor}
|
||||
config={config}
|
||||
value={index === 0 ? body0 : body1}
|
||||
className='mt-4 w-full'
|
||||
onBlur={value => {
|
||||
index === 0 ? setBody0(value) : setBody1(value)
|
||||
setValue(`locales.${index}.body`, value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
<Button type='submit' className='float-right my-4 w-[200px]'>
|
||||
Створити
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
Home,
|
||||
Inbox,
|
||||
LayoutList,
|
||||
List,
|
||||
Newspaper,
|
||||
Plus,
|
||||
ScanBarcode,
|
||||
Search,
|
||||
@@ -44,6 +46,16 @@ const items = [
|
||||
title: 'Товари',
|
||||
url: `${ADMIN_DASHBOARD_PATH}/product`,
|
||||
icon: ScanBarcode
|
||||
},
|
||||
{
|
||||
title: 'Сутність',
|
||||
url: `${ADMIN_DASHBOARD_PATH}/entity`,
|
||||
icon: Newspaper
|
||||
},
|
||||
{
|
||||
title: 'Замовлення',
|
||||
url: `${ADMIN_DASHBOARD_PATH}/order`,
|
||||
icon: List
|
||||
}
|
||||
// {
|
||||
// title: 'Search',
|
||||
@@ -70,7 +82,7 @@ export function AdminSidebar() {
|
||||
<SidebarGroupContent>SidebarGroupAction</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Application</SidebarGroupLabel>
|
||||
{/*<SidebarGroupLabel>Application</SidebarGroupLabel>*/}
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map(item => (
|
||||
@@ -86,7 +98,7 @@ export function AdminSidebar() {
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<Collapsible defaultOpen className='group/collapsible'>
|
||||
<Collapsible defaultOpen className='group/collapsible' hidden={true}>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel asChild>
|
||||
<CollapsibleTrigger>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {Order} from '@prisma/client'
|
||||
import {useTranslations} from 'next-intl'
|
||||
|
||||
import {getOrdersByUserId} from '@/actions/admin/order'
|
||||
import {SignOutButton} from '@/components/auth/forms/sign-out-button'
|
||||
import CabinetButton from '@/components/shared/header/cabinet-button'
|
||||
import {type SingedInSession} from '@/lib/permission'
|
||||
@@ -11,7 +13,7 @@ import {
|
||||
} from '@/ui/collapsible'
|
||||
import {Separator} from '@/ui/separator'
|
||||
|
||||
export default function CabinetIndex({
|
||||
export default async function CabinetIndex({
|
||||
slug,
|
||||
session
|
||||
}: {
|
||||
@@ -19,6 +21,7 @@ export default function CabinetIndex({
|
||||
session: SingedInSession | null
|
||||
}) {
|
||||
const t = useTranslations('cabinet')
|
||||
const orders = await getOrdersByUserId(parseInt(session?.user.id as string))
|
||||
|
||||
return (
|
||||
<div className='my-8'>
|
||||
@@ -40,10 +43,24 @@ export default function CabinetIndex({
|
||||
{t('personal-information.title')}
|
||||
</h1>
|
||||
<Separator className='my-4' />
|
||||
{orders.map((order: Order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className='flex items-center justify-start gap-x-9 gap-y-6'
|
||||
>
|
||||
<div>{order.orderNo}</div>
|
||||
<div>
|
||||
{order.firstName} {order.surname}
|
||||
</div>
|
||||
<div>{order.phone}</div>
|
||||
<div>{order.email}</div>
|
||||
<div>{order.notes}</div>
|
||||
</div>
|
||||
))}
|
||||
{/*<BasicEditor placeholder={'type something'} />*/}
|
||||
{/*<Separator className='my-4' />*/}
|
||||
{slug ? <code>{slug[0]}</code> : <pre>{dump(session)}</pre>}
|
||||
<Collapsible>
|
||||
{/*{slug ? <code>{slug[0]}</code> : <pre>{dump(session)}</pre>}*/}
|
||||
{/*<Collapsible>
|
||||
<CollapsibleTrigger>
|
||||
Can I use this in my project?
|
||||
</CollapsibleTrigger>
|
||||
@@ -51,7 +68,7 @@ export default function CabinetIndex({
|
||||
Yes. Free to use for personal and commercial projects. No
|
||||
attribution required.
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</Collapsible>*/}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,48 @@
|
||||
input.bw-cart-item-counter{
|
||||
font-size: 36px;
|
||||
background: chocolate;
|
||||
.bwOrderForm {
|
||||
& > h2, & > h3 {
|
||||
@apply text-center text-brand-violet text-2xl;
|
||||
}
|
||||
|
||||
[role="tablist"] {
|
||||
@apply bg-transparent rounded-none shadow-none m-0 pb-5 border-b-2 border-brand-violet h-[unset];
|
||||
|
||||
button {
|
||||
@apply justify-start rounded-none font-normal text-xl pl-0;
|
||||
}
|
||||
|
||||
[data-state=active] {
|
||||
@apply text-brand-violet shadow-none;
|
||||
}
|
||||
[data-state=inactive] {
|
||||
@apply text-gray-600 ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.registeredForm{
|
||||
/*@apply bg-brand-yellow-100;*/
|
||||
|
||||
& > h2, & > h3 {
|
||||
@apply text-brand-violet text-2xl mt-9 mb-2;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
@apply md:flex md:items-start md:justify-between md:gap-8
|
||||
}
|
||||
|
||||
label {
|
||||
@apply text-lg font-normal block mt-8 leading-none;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply border-t-0 border-r-0 border-l-0 border-stone-400 rounded-none text-foreground text-lg p-0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@apply min-h-[72px] w-full border-b border-stone-400 p-2 ;
|
||||
}
|
||||
|
||||
[role="combobox"] {
|
||||
@apply w-full text-lg pl-0 text-foreground border-t-0 h-[unset] pb-2 border-r-0 border-l-0 rounded-none shadow-none border-stone-400;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// import styles from '@/components/pages/cart/cart.module.scss'
|
||||
import {Minus, Plus} from 'lucide-react'
|
||||
import {Minus, Plus, X} from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import {Link} from '@/i18n/routing'
|
||||
import useCartStore, {CartItem} from '@/store/cart-store'
|
||||
import {Button} from '@/ui/button'
|
||||
|
||||
export default function CartItems() {
|
||||
const {cartItems} = useCartStore()
|
||||
|
||||
export default function CartItems({cartItems}: {cartItems: CartItem[]}) {
|
||||
const {increaseQuantity, decreaseQuantity, removeItemFromCart} =
|
||||
useCartStore()
|
||||
|
||||
const onIncreaseQuantity = (productId: number) => {
|
||||
increaseQuantity(productId)
|
||||
}
|
||||
@@ -23,22 +21,33 @@ export default function CartItems() {
|
||||
removeItemFromCart(productId)
|
||||
}
|
||||
|
||||
if (cartItems && cartItems.length > 0) {
|
||||
return (
|
||||
<>
|
||||
{cartItems?.map((item: CartItem, i: number) => (
|
||||
<div className='my-4 flex items-center' key={i}>
|
||||
return (
|
||||
<>
|
||||
{cartItems?.map((item: CartItem, i: number) => (
|
||||
<article key={i} className='bxg-emerald-200 mb-6'>
|
||||
<h3 className='bxg-brand-yellow-300 flex w-full items-center justify-between text-foreground'>
|
||||
<div className='text-lg font-medium'>{item.title}</div>
|
||||
<div className='w-16 flex-none text-right'>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
className='rounded-0 h-[3rem] w-[3rem] px-0 text-brand-violet'
|
||||
onClick={() => onRemoveItem(item.id)}
|
||||
/*title={t('clear_cart')}*/
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
</h3>
|
||||
<div className='flex items-center'>
|
||||
<div className='col'>
|
||||
{item.title}
|
||||
<Image
|
||||
src={(item?.image || '').replace('.jpg', '-thumb.jpg')}
|
||||
alt=''
|
||||
width={96}
|
||||
height={96}
|
||||
className='rounded-md border'
|
||||
width={64}
|
||||
height={64}
|
||||
style={{
|
||||
width: '96px',
|
||||
height: '96px',
|
||||
width: '64px',
|
||||
height: '64px',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
/>
|
||||
@@ -47,17 +56,17 @@ export default function CartItems() {
|
||||
<div className='flex w-16 flex-none items-center justify-center'>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className='rounded-0 h-[3rem] w-[3rem] text-2xl leading-none text-brand-violet'
|
||||
className='rounded-0 h-[3rem] w-[3rem] text-brand-violet'
|
||||
onClick={() => onDecreaseQuantity(item.id)}
|
||||
>
|
||||
<Minus />
|
||||
</Button>
|
||||
<div className='mx-4 text-xl font-bold leading-none text-brand-violet'>
|
||||
<div className='mx-4 text-xl font-bold text-brand-violet'>
|
||||
{item.quantity}
|
||||
</div>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className='rounded-0 h-[3rem] w-[3rem] text-2xl leading-none text-brand-violet'
|
||||
className='rounded-0 h-[3rem] w-[3rem] text-brand-violet'
|
||||
onClick={() => onIncreaseQuantity(item.id)}
|
||||
>
|
||||
<Plus />
|
||||
@@ -68,19 +77,8 @@ export default function CartItems() {
|
||||
{(item.price * item.quantity).toFixed(2)} грн
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='flex h-72 flex-col items-center justify-center'>
|
||||
<h2 className='mb-5 mt-10 text-3xl font-bold'>Cart is Empty</h2>
|
||||
<Link
|
||||
href={'/catalog'}
|
||||
className='rounded-md bg-orange-500 px-6 py-2 text-white'
|
||||
>
|
||||
Продовжити покупки
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
290
components/pages/cart/registered-order-form.tsx
Normal file
290
components/pages/cart/registered-order-form.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
'use client'
|
||||
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import {DeliveryOption} from '@prisma/client'
|
||||
import {useLocale} from 'next-intl'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
import {z} from 'zod'
|
||||
|
||||
import {onPlacingOrder} from '@/actions/admin/place-order'
|
||||
import NovaPost from '@/app/[locale]/(root)/(shop)/cart/nova-post'
|
||||
import SearchAddress from '@/components/pages/cart/search-address'
|
||||
import {
|
||||
DeliveryOptionTypeDescription,
|
||||
createOrderFormSchema
|
||||
} from '@/lib/schemas/admin/order'
|
||||
import {dump} from '@/lib/utils'
|
||||
import useCartStore from '@/store/cart-store'
|
||||
import {SessionUser} from '@/types/auth'
|
||||
import {Button} from '@/ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/ui/form'
|
||||
import {Input} from '@/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/ui/select'
|
||||
|
||||
export default function RegisteredOrderForm({
|
||||
styles,
|
||||
user,
|
||||
onSubmitHandler
|
||||
}: {
|
||||
styles: string
|
||||
user?: SessionUser | null
|
||||
onSubmitHandler: any
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
const [deliveryOption, setDeliveryOption] = useState('')
|
||||
|
||||
const locale = useLocale()
|
||||
const [warehouseRef, setWarehouseRef] = useState(JSON.stringify({}))
|
||||
|
||||
const warehouseSubmit = (warehouse: any) => {
|
||||
setWarehouseRef(
|
||||
Object.keys(warehouse).length > 0
|
||||
? JSON.stringify(warehouse)
|
||||
: JSON.stringify({})
|
||||
)
|
||||
|
||||
setValue(
|
||||
'address',
|
||||
Object.keys(warehouse).length > 0
|
||||
? JSON.stringify(warehouse)
|
||||
: JSON.stringify({})
|
||||
)
|
||||
}
|
||||
const {cartItems, clearCart} = useCartStore()
|
||||
const form = useForm<z.infer<typeof createOrderFormSchema>>({
|
||||
resolver: zodResolver(createOrderFormSchema),
|
||||
mode: 'onBlur',
|
||||
defaultValues: {
|
||||
user_id: user ? user.id.toString() : '',
|
||||
is_quick: false,
|
||||
lang: locale,
|
||||
first_name: '',
|
||||
surname: '',
|
||||
delivery_option: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: warehouseRef,
|
||||
notes: '',
|
||||
details: JSON.stringify(cartItems)
|
||||
}
|
||||
})
|
||||
|
||||
const {register, setValue} = form
|
||||
|
||||
useEffect(() => {
|
||||
register('delivery_option')
|
||||
register('address')
|
||||
register('details')
|
||||
}, [register])
|
||||
|
||||
const deliveryOptionHandler = (value: string) => {
|
||||
setDeliveryOption(value)
|
||||
setValue('delivery_option', value)
|
||||
}
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof createOrderFormSchema>) => {
|
||||
setLoading(true)
|
||||
setValue('details', JSON.stringify(cartItems))
|
||||
onPlacingOrder(values).then((res: any) => {
|
||||
if (res?.error) {
|
||||
setError(res?.error)
|
||||
setSuccess('')
|
||||
setLoading(false)
|
||||
toast.error(res?.error)
|
||||
} else {
|
||||
setSuccess(res?.success as string)
|
||||
setError('')
|
||||
setLoading(false)
|
||||
clearCart()
|
||||
toast.success(res?.success)
|
||||
}
|
||||
|
||||
onSubmitHandler(res)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form className={styles} action='' onSubmit={form.handleSubmit(onSubmit)}>
|
||||
{/*<pre>{dump(user)}</pre>*/}
|
||||
<h2>1. {locale !== 'ru' ? 'Особисті дані' : 'Личные данные'}</h2>
|
||||
<fieldset>
|
||||
<div className='md:w-1/2'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='first_name'
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>{locale !== 'ru' ? "Ім'я" : 'Имя'}*</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='text' placeholder='' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='md:w-1/2'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='surname'
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{locale !== 'ru' ? 'Прізвище' : 'Фамилия'}*
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='text' placeholder='' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<div className='md:w-1/2'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='phone'
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Телефон*</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='text' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='md:w-1/2'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>E-mail*</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='text' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<h2>
|
||||
2.{' '}
|
||||
{locale !== 'ru'
|
||||
? 'Інформація про доставку'
|
||||
: 'Информация о доставке'}
|
||||
</h2>
|
||||
<fieldset>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='delivery_option'
|
||||
render={({field}) => (
|
||||
<FormItem className='block w-full'>
|
||||
<FormLabel>
|
||||
{locale !== 'ru'
|
||||
? 'Варіанти доставки'
|
||||
: 'Варианты доставки'}{' '}
|
||||
</FormLabel>
|
||||
<Select
|
||||
/*onValueChange={field.onChange}*/
|
||||
onValueChange={deliveryOptionHandler}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
locale !== 'ru'
|
||||
? 'Оберіть варіант доставки'
|
||||
: 'Выберите вариант доставки'
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{Object.keys(DeliveryOption).map(
|
||||
(option: string, index: number) => (
|
||||
<SelectItem key={index} value={option}>
|
||||
{DeliveryOptionTypeDescription[option] || option}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage className='ml-3' />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
{deliveryOption === 'NP' && (
|
||||
<NovaPost onSelectHandler={warehouseSubmit} />
|
||||
)}
|
||||
|
||||
{deliveryOption === 'COURIER' && (
|
||||
/*<NovaPost onWarehouseSelect={warehouseSubmit} />*/
|
||||
<SearchAddress onSelectHandler={warehouseSubmit} />
|
||||
)}
|
||||
|
||||
{deliveryOption === 'PICKUP' && (
|
||||
<div className='py-6 text-lg'>Дані де і коли можна забрати</div>
|
||||
)}
|
||||
|
||||
<fieldset>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='notes'
|
||||
render={({field}) => (
|
||||
<FormItem className='block w-full'>
|
||||
<FormLabel>
|
||||
{locale !== 'ru'
|
||||
? 'Додаткова інформація'
|
||||
: 'Дополнительная информация'}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<textarea {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
{/*<pre>{warehouseRef}</pre>*/}
|
||||
<Button
|
||||
type='submit'
|
||||
size={'lg'}
|
||||
className='float-right mx-auto mb-6 mt-16 h-[unset] w-[200px] py-2 text-xl text-brand-violet'
|
||||
>
|
||||
{locale !== 'ru' ? 'Оформити замовлення' : 'Оформить заказ'}
|
||||
</Button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
152
components/pages/cart/search-address.tsx
Normal file
152
components/pages/cart/search-address.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from 'cmdk'
|
||||
import {Check, ChevronsUpDown, MapPinCheck, MapPinPlus} from 'lucide-react'
|
||||
import {useLocale, useTranslations} from 'next-intl'
|
||||
import {useState} from 'react'
|
||||
import {useDebouncedCallback} from 'use-debounce'
|
||||
|
||||
import {
|
||||
type Settlement,
|
||||
Street,
|
||||
formatSettlement,
|
||||
getApi
|
||||
} from '@/lib/nova-post-helper'
|
||||
import {cn, dump} from '@/lib/utils'
|
||||
import {Button} from '@/ui/button'
|
||||
import {Command} from '@/ui/command'
|
||||
import {Popover, PopoverContent, PopoverTrigger} from '@/ui/popover'
|
||||
|
||||
const url = '/api/nova-post'
|
||||
|
||||
export default function SearchAddress({
|
||||
onSelectHandler
|
||||
}: {
|
||||
onSelectHandler: any
|
||||
}) {
|
||||
const t = useTranslations('cart.post')
|
||||
const locale = useLocale()
|
||||
const [streets, setStreets] = useState([])
|
||||
const [streetsOpen, setStreetsOpen] = useState(false)
|
||||
const [streetsValue, setStreetsValue] = useState('')
|
||||
|
||||
const handleStreetSearch = useDebouncedCallback(
|
||||
async (e: string): Promise<void> => {
|
||||
if (e.length < 3) {
|
||||
setStreets([])
|
||||
return
|
||||
}
|
||||
|
||||
const response = await getApi(url + `?scope=streets&q=` + encodeURI(e))
|
||||
|
||||
if (response.ok) {
|
||||
let json = JSON.parse(JSON.stringify(await response.json()))
|
||||
const {Addresses} = json[0]
|
||||
setStreets(Addresses)
|
||||
} else {
|
||||
setStreets([])
|
||||
}
|
||||
},
|
||||
1000
|
||||
)
|
||||
|
||||
const streetDescription = (streetsValue: string): string => {
|
||||
const street: Street | undefined = streets.find(
|
||||
(street: Street) => street.Present === streetsValue
|
||||
)
|
||||
|
||||
if (!street) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return streetsValue
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-2'>
|
||||
{/*<pre>{dump(streets[0]['Addresses'])}</pre>*/}
|
||||
<div>
|
||||
<Popover open={streetsOpen} onOpenChange={setStreetsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
/*aria-expanded={open}*/
|
||||
className='w-full justify-between border border-brand-violet'
|
||||
>
|
||||
<span className='inline-flex items-center gap-x-3'>
|
||||
{streetsValue ? (
|
||||
<>
|
||||
<MapPinCheck />
|
||||
{streetDescription(streetsValue)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MapPinPlus />
|
||||
{locale !== 'ru' ? 'Шукати вулицю' : 'Искать улицу'}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<ChevronsUpDown className='opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-[640px] p-2'>
|
||||
<Command>
|
||||
<CommandInput
|
||||
className='border border-brand-violet p-2'
|
||||
placeholder={locale !== 'ru' ? 'Почати пошук' : 'Начать поиск'}
|
||||
onValueChange={(e: string) => handleStreetSearch(e)}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className='my-1'>{t('notFount')}</CommandEmpty>
|
||||
<CommandGroup className='max-h-[320px] w-full overflow-y-auto'>
|
||||
{streets.map((street: Street, index: number) => (
|
||||
<CommandItem
|
||||
className='my-2 flex'
|
||||
key={index}
|
||||
value={street?.Present}
|
||||
onSelect={(currentValue: string) => {
|
||||
setStreetsValue(
|
||||
currentValue === streetsValue ? '' : currentValue
|
||||
)
|
||||
|
||||
onSelectHandler(
|
||||
currentValue === streetsValue
|
||||
? {}
|
||||
: {
|
||||
Ref: street.SettlementStreetRef,
|
||||
Description: street.Present,
|
||||
DescriptionRu:
|
||||
street.SettlementStreetDescriptionRu
|
||||
}
|
||||
)
|
||||
|
||||
setStreetsOpen(false)
|
||||
}}
|
||||
>
|
||||
{street?.Present}
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
streetsValue === street?.Present
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -25,22 +25,23 @@ import {Link} from '@/i18n/routing'
|
||||
import {getMetaOfFile} from '@/lib/config/resources'
|
||||
import {getProductResources} from '@/lib/data/models/product'
|
||||
import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas'
|
||||
import {db} from '@/lib/db/prisma/client'
|
||||
import {dump, thisLocale, toPrice} from '@/lib/utils'
|
||||
import {Button} from '@/ui/button'
|
||||
import {Separator} from '@/ui/separator'
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/ui/tabs'
|
||||
|
||||
export default async function ProductPageIndex({id}: {id: string}) {
|
||||
const t = await getTranslations('Common')
|
||||
|
||||
const data: CategoryPageSqlSchema[] = await db.$queryRawTyped(
|
||||
getProductByIdWitData(id)
|
||||
)
|
||||
|
||||
export default async function ProductPageIndex({
|
||||
data,
|
||||
id
|
||||
}: {
|
||||
data: CategoryPageSqlSchema[]
|
||||
id: string
|
||||
}) {
|
||||
const locale = await thisLocale(data)
|
||||
if (!locale) notFound()
|
||||
|
||||
const t = await getTranslations('Common')
|
||||
|
||||
const resources: ProductResource[] | null = await getProductResources(
|
||||
parseInt(id)
|
||||
)
|
||||
@@ -70,11 +71,11 @@ export default async function ProductPageIndex({id}: {id: string}) {
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className='flex w-[82%] items-center justify-between'>
|
||||
<h1 className='my-4 text-3xl font-bold text-brand-violet-950'>
|
||||
<div className='flex w-full items-center justify-between'>
|
||||
<h1 className='font-heading mt-4 text-3xl font-semibold'>
|
||||
{locale.title}
|
||||
</h1>
|
||||
<AddCartButton
|
||||
{/*<AddCartButton
|
||||
product={{
|
||||
id: locale.productId,
|
||||
quantity: 1,
|
||||
@@ -82,9 +83,9 @@ export default async function ProductPageIndex({id}: {id: string}) {
|
||||
price: toPrice(locale.price),
|
||||
image: locale.image
|
||||
}}
|
||||
/>
|
||||
/>*/}
|
||||
</div>
|
||||
<Separator className='my-4 w-[82%] border-b border-brand-violet' />
|
||||
<Separator className='my-4 h-0 border-b-2 border-brand-violet' />
|
||||
<ProductCarousel images={resources} title={locale.title} />
|
||||
<Tabs defaultValue='article' className=''>
|
||||
<TabsList className='grid w-full grid-cols-2'>
|
||||
|
||||
@@ -11,7 +11,7 @@ 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='bw-above flex h-[51px] w-full items-end justify-center bg-brand-violet'>
|
||||
<div className='mx-0 mb-0.5'>
|
||||
<Image
|
||||
width={72.79}
|
||||
|
||||
@@ -2,26 +2,40 @@
|
||||
|
||||
// 31:47
|
||||
import {ChevronUp} from 'lucide-react'
|
||||
import {useLocale} from 'next-intl'
|
||||
|
||||
import SocialMediaPanel from '@/components/shared/social-media-panel'
|
||||
import {Link} from '@/i18n/routing'
|
||||
import {Button} from '@/ui/button'
|
||||
|
||||
export default function Footer() {
|
||||
const locale = useLocale()
|
||||
|
||||
return (
|
||||
<footer className='bg-brand-violet w-full py-3 text-white'>
|
||||
<footer className='w-full bg-brand-violet py-3 text-white'>
|
||||
<div className='container flex items-center justify-between'>
|
||||
<div>Політика конфіденційності</div>
|
||||
<div>Договір оферти</div>
|
||||
<div>
|
||||
<Link href={'/privacy-policy'}>
|
||||
{locale === 'uk'
|
||||
? 'Політика конфіденційності'
|
||||
: 'Политика конфиденциальности'}
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link href={'/offer-contract'}>
|
||||
{locale === 'uk' ? 'Договір оферти' : 'Договор оферты'}
|
||||
</Link>
|
||||
</div>
|
||||
<div>Доставка і повернення</div>
|
||||
<div>Контакти</div>
|
||||
<SocialMediaPanel color='#fff' />
|
||||
<Button
|
||||
{/*<Button
|
||||
variant='ghost'
|
||||
className='bg-brand-violet rounded-none'
|
||||
className='rounded-none bg-brand-violet'
|
||||
onClick={() => window.scrollTo({top: 0, behavior: 'smooth'})}
|
||||
>
|
||||
<ChevronUp className='mr-2 h-4 w-4' />
|
||||
</Button>
|
||||
</Button>*/}
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
|
||||
@@ -9,13 +9,13 @@ export default function HeaderControls() {
|
||||
const t = useTranslations('cart')
|
||||
|
||||
return (
|
||||
<div className='flex w-full justify-end gap-x-6 text-brand-violet'>
|
||||
<div className='flex w-full items-center justify-end gap-x-2 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>
|
||||
{/*<span className='font1-bold text-sm'>{t('favorites')}</span>*/}
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
|
||||
@@ -4,21 +4,30 @@ import {ShoppingCartIcon} from 'lucide-react'
|
||||
import {useTranslations} from 'next-intl'
|
||||
|
||||
import {Link} from '@/i18n/routing'
|
||||
import useCartStore from '@/store/cart-store'
|
||||
import useCartStore, {CartItem} from '@/store/cart-store'
|
||||
|
||||
export default function HeaderShoppingCartIcon() {
|
||||
const t = useTranslations('cart')
|
||||
const t = useTranslations('Common')
|
||||
const {cartItems} = useCartStore()
|
||||
const cartCount = cartItems.length
|
||||
const cartCount = cartItems.reduce(
|
||||
(accumulator: number, item: CartItem) => accumulator + item.quantity,
|
||||
0
|
||||
)
|
||||
|
||||
return (
|
||||
<Link href={'/cart' as never} className='header-button' aria-label='Кошик'>
|
||||
<Link
|
||||
href={'/cart' as never}
|
||||
className='header-button relative'
|
||||
aria-label={t('basket')}
|
||||
>
|
||||
<button className='flex flex-col items-center' role='button'>
|
||||
<ShoppingCartIcon className='h-[21px] w-[21px]' />
|
||||
|
||||
<span className='font1-bold text-sm'>
|
||||
{t('basket')} [{cartCount}]
|
||||
</span>
|
||||
{cartCount > 0 && (
|
||||
<div className='absolute -right-1 -top-1 h-[20px] w-[20px] rounded-full border border-brand-violet bg-brand-yellow pl-0 pr-[1px] text-[0.625rem] font-bold leading-[18px]'>
|
||||
{cartCount}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import {useLocale, useTranslations} from 'next-intl'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
// TODO: Link throwing no connection error in this component; React 19 Bug
|
||||
// import {Link} from '@/i18n/routing'
|
||||
import {APP_NAME} from '@/lib/constants'
|
||||
import logoImg from '@/public/images/logo.svg'
|
||||
|
||||
export default function Logo() {
|
||||
const t = useTranslations('Common')
|
||||
const locale = useLocale()
|
||||
const ar = 121 / 192
|
||||
const w = 112
|
||||
|
||||
return (
|
||||
<div className='mt-0.5 flex items-center justify-center'>
|
||||
<Link
|
||||
href='/'
|
||||
<a
|
||||
href={locale !== 'ru' ? '/' : '/ru'}
|
||||
className='m-1 flex cursor-pointer items-center pt-[7px] text-2xl font-extrabold outline-0'
|
||||
aria-label={t('home')}
|
||||
>
|
||||
<Image
|
||||
src={logoImg}
|
||||
@@ -23,7 +28,7 @@ export default function Logo() {
|
||||
alt={`${APP_NAME} logo`}
|
||||
className='w-[131]'
|
||||
/>
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ export default function LocaleSwitcher() {
|
||||
path: '/',
|
||||
sameSite: 'Lax'
|
||||
})
|
||||
router.replace(window.location.pathname.replace(/^\/ru/, ''), {locale})
|
||||
let path = window.location.pathname.replace(/^\/ru/, '')
|
||||
if (path === '') path = '/'
|
||||
router.replace(path, {locale})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,7 +22,11 @@ export default function NavbarMenu() {
|
||||
<div className='flex items-center justify-between'>
|
||||
{data[locale === 'uk' ? 'headerMenus' : 'headerMenusRus'].map(
|
||||
item => (
|
||||
<Link href='/about-us' className='' key={item.name}>
|
||||
<Link
|
||||
href='/about-us'
|
||||
className='hover:[text-shadow:_0_1px_2px_rgb(99_102_241_/_0.6)]'
|
||||
key={item.name}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function AppCatalogRender(data: {items: Category[]}) {
|
||||
|
||||
return (
|
||||
<div className='flex w-full justify-center'>
|
||||
<div className='bw-dd-menu group inline-block w-full'>
|
||||
<div className='bw-dd-menu b group inline-block w-full'>
|
||||
<Link href='/catalog'>
|
||||
<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>
|
||||
@@ -29,12 +29,12 @@ export default function AppCatalogRender(data: {items: Category[]}) {
|
||||
<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'>
|
||||
{data?.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'
|
||||
className='pay-2.5 cursor-pointer rounded-none pl-3 pr-1.5 text-sm font-medium hover:bg-[#442d88]/10 xl:py-2'
|
||||
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]'
|
||||
className='flex-1 pr-1 leading-none xl:leading-[1.275]'
|
||||
href={`/category/${item.locales[locale === 'uk' ? 0 : 1].slug}`}
|
||||
>
|
||||
{item.locales[locale === 'uk' ? 0 : 1].title}
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function CardBuyButton({
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`mr-2 ${isIcon ? '' : 'w-[80px]'} grow-0 shadow-white hover:shadow-md hover:shadow-brand-violet/50`}
|
||||
className={`mr-1.5 ${isIcon ? '' : 'w-[80px]'} z-50 grow-0 shadow-white hover:shadow-md hover:shadow-brand-violet/50`}
|
||||
onClick={() => addItemToCart(item)}
|
||||
>
|
||||
{isIcon ? (
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import {ProductResource} from '@prisma/client'
|
||||
import {Star, StarHalf} from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import CardBuyButton from '@/components/shared/store/card-buy-button'
|
||||
import RateStars from '@/components/shared/store/stars'
|
||||
import {Link} from '@/i18n/routing'
|
||||
import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas'
|
||||
import {Button} from '@/ui/button'
|
||||
import {Card, CardContent, CardFooter} from '@/ui/card'
|
||||
import {CarouselItem} from '@/ui/carousel'
|
||||
|
||||
export default function FeatureCardFront({
|
||||
card
|
||||
@@ -18,7 +14,7 @@ export default function FeatureCardFront({
|
||||
return (
|
||||
<Card className='relative aspect-card overflow-hidden border-[2px] border-brand-violet transition duration-300 hover:shadow-lg hover:shadow-brand-violet/50'>
|
||||
<Link href={`/product/${card.productId}-${card.slug}`}>
|
||||
<CardContent className='relative flex h-[81%] flex-col justify-between overflow-hidden pt-4'>
|
||||
<CardContent className='relative flex h-[76%] flex-col justify-between overflow-hidden pt-4'>
|
||||
{/*<CarouselItem>*/}
|
||||
<Image
|
||||
className='transition duration-300 hover:scale-110'
|
||||
@@ -39,12 +35,12 @@ export default function FeatureCardFront({
|
||||
{/*</CarouselItem>*/}
|
||||
</CardContent>
|
||||
</Link>
|
||||
<div className='bw-card-footer flex h-[19%] items-center justify-between border-t-[2px] border-brand-violet px-4'>
|
||||
<div className='bw-card-footer flex h-[24%] items-center justify-between border-t-[2px] border-brand-violet px-2'>
|
||||
<div className=''>
|
||||
<p className='ml-2 border-b border-b-brand-violet pl-2 pr-6 text-[16px]'>
|
||||
<p className='font-heading ml-1 border-b border-b-brand-violet pb-0.5 pl-1 pr-3'>
|
||||
{card.title}
|
||||
</p>
|
||||
<p className='pl-4 text-[16px] font-bold text-brand-violet'>
|
||||
<p className='pl-2 text-[18px] font-bold text-brand-violet'>
|
||||
{parseFloat(card.price as string).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -55,17 +51,9 @@ export default function FeatureCardFront({
|
||||
title: card.title,
|
||||
price: parseFloat(card.price as string).toFixed(2)
|
||||
}}
|
||||
isIcon={true}
|
||||
isIcon={false}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// id: number
|
||||
// quantity: number
|
||||
// title: string
|
||||
// price: string | any
|
||||
// image?: string | null
|
||||
// imageWidth?: number | null
|
||||
// imageHeight?: number | null
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import {Star, StarHalf} from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import CardBuyButton from '@/components/shared/store/card-buy-button'
|
||||
import RateStars from '@/components/shared/store/stars'
|
||||
import {Link} from '@/i18n/routing'
|
||||
import {Button} from '@/ui/button'
|
||||
import {Card, CardContent, CardFooter} from '@/ui/card'
|
||||
|
||||
//import {CarouselItem} from '@/ui/carousel'
|
||||
|
||||
export default function FeatureCard({card}: {card: any}) {
|
||||
return (
|
||||
<Card className='relative aspect-card overflow-hidden border-[2px] border-brand-violet transition duration-300 hover:shadow-lg hover:shadow-brand-violet/50'>
|
||||
<Link href={`/product/${card.productId}-${card.slug}`}>
|
||||
<CardContent className='relative flex h-[81%] flex-col justify-between overflow-hidden pt-4'>
|
||||
{/*<CarouselItem>*/}
|
||||
<CardContent className='relative flex h-[76%] flex-col justify-between overflow-hidden pt-4'>
|
||||
<Image
|
||||
className='transition duration-300 hover:scale-110'
|
||||
src={card.image}
|
||||
@@ -33,12 +28,12 @@ export default function FeatureCard({card}: {card: any}) {
|
||||
<RateStars />
|
||||
</CardContent>
|
||||
</Link>
|
||||
<div className='bw-card-footer flex h-[19%] items-center justify-between border-t-[2px] border-brand-violet px-4'>
|
||||
<div className='bw-card-footer flex h-[24%] items-center justify-between border-t-[2px] border-brand-violet px-1'>
|
||||
<div className=''>
|
||||
<p className='ml-2 border-b border-b-brand-violet pl-2 pr-6'>
|
||||
<p className='font-heading ml-1 border-b border-b-brand-violet pb-0.5 pl-1 pr-3'>
|
||||
{card.title}
|
||||
</p>
|
||||
<p className='pl-4 text-[18px] font-bold text-brand-violet'>
|
||||
<p className='pl-2 text-[18px] font-bold text-brand-violet'>
|
||||
{parseFloat(card.price).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,16 @@ import {Star} from 'lucide-react'
|
||||
|
||||
const startStroke = 1.5
|
||||
const color = '#ffd139'
|
||||
const size = 16
|
||||
|
||||
export default function RateStars() {
|
||||
return (
|
||||
<div className='bw-rating absolute bottom-2 left-4 inline-flex h-[32px] items-center gap-1'>
|
||||
<Star strokeWidth={startStroke} color={color} fill={color} />
|
||||
<Star strokeWidth={startStroke} color={color} fill={color} />
|
||||
<Star strokeWidth={startStroke} color={color} fill={color} />
|
||||
<Star strokeWidth={startStroke} color={color} />
|
||||
<Star strokeWidth={startStroke} color={color} />
|
||||
<div className='bw-rating absolute bottom-1 left-3 inline-flex h-[32px] items-center gap-1'>
|
||||
<Star size={size} strokeWidth={startStroke} color={color} fill={color} />
|
||||
<Star size={size} strokeWidth={startStroke} color={color} fill={color} />
|
||||
<Star size={size} strokeWidth={startStroke} color={color} fill={color} />
|
||||
<Star size={size} strokeWidth={startStroke} color={color} />
|
||||
<Star size={size} strokeWidth={startStroke} color={color} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
55
components/shared/terms/index.tsx
Normal file
55
components/shared/terms/index.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import {Entity, EntityLocale, type Lang} from '@prisma/client'
|
||||
import {useLocale} from 'next-intl'
|
||||
|
||||
import {getBlockEntity} from '@/actions/admin/entity'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger
|
||||
} from '@/components/ui/accordion'
|
||||
import {locales} from '@/i18n/routing'
|
||||
import {EntityTerm} from '@/lib/schemas/admin/entity'
|
||||
import {dump, thisLocale} from '@/lib/utils'
|
||||
|
||||
export default async function Terms() {
|
||||
const locale = useLocale()
|
||||
const terms = await getBlockEntity('terms')
|
||||
|
||||
return (
|
||||
<section className='container mb-4 mt-12'>
|
||||
{/*<pre>{dump(terms)}</pre>*/}
|
||||
<div className='bw-terms-section mx-auto max-w-[1080px]'>
|
||||
<Accordion
|
||||
type='single'
|
||||
collapsible
|
||||
className='flex w-full flex-wrap justify-between gap-y-4'
|
||||
>
|
||||
{terms.map(async (term: any, index: number) => {
|
||||
const {locales} = term
|
||||
const locale: EntityLocale = await thisLocale(locales)
|
||||
|
||||
const {title, body} = locale
|
||||
return (
|
||||
<AccordionItem
|
||||
key={index}
|
||||
value={`item-${index}`}
|
||||
className='bw-accordion-item'
|
||||
>
|
||||
<AccordionTrigger className='bw-accordion-trigger'>
|
||||
<div className='flex-grow'>{title}</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className='bw-accordion-content'>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{__html: body as string}}
|
||||
></span>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)
|
||||
})}
|
||||
{/*<div className='bw-accordion-item table-cell'></div>*/}
|
||||
</Accordion>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
28
components/shared/youtube-component.tsx
Normal file
28
components/shared/youtube-component.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import YouTube, {YouTubeProps} from 'react-youtube'
|
||||
|
||||
export default function YoutubeComponent({id}: {id: string}) {
|
||||
const onPlayerReady: YouTubeProps['onReady'] = e => {
|
||||
e.target.pauseVideo()
|
||||
}
|
||||
|
||||
const opts: YouTubeProps['opts'] = {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
playerVars: {
|
||||
// https://developers.google.com/youtube/player_parameters
|
||||
autoplay: 0
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<YouTube
|
||||
id={`video-yt-${id}`}
|
||||
videoId={id}
|
||||
opts={opts}
|
||||
onReady={onPlayerReady}
|
||||
iframeClassName='bw-yt-video'
|
||||
/>
|
||||
)
|
||||
}
|
||||
57
components/ui/accordion.tsx
Normal file
57
components/ui/accordion.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
153
components/ui/command.tsx
Normal file
153
components/ui/command.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
33
components/ui/popover.tsx
Normal file
33
components/ui/popover.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
44
components/ui/radio-group.tsx
Normal file
44
components/ui/radio-group.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
})
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-3.5 w-3.5 fill-primary" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
})
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
Reference in New Issue
Block a user