stuff done

This commit is contained in:
2025-03-11 02:54:09 +02:00
parent 58e7ed2f06
commit 516b45fad9
90 changed files with 2950 additions and 9458 deletions

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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