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

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