stuff done
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import {auth} from '@/auth'
|
||||
import AdminPermission from '@/components/(protected)/admin/auth/permission'
|
||||
import {CreateForm} from '@/components/(protected)/admin/category/create-form'
|
||||
import ProductCreateEditForm from '@/components/(protected)/admin/product/create-edit-form'
|
||||
import {dump} from '@/lib/utils'
|
||||
|
||||
export default async function Page({
|
||||
@@ -12,7 +14,12 @@ export default async function Page({
|
||||
|
||||
switch ((slug || [])[0]) {
|
||||
case 'create':
|
||||
return <CreateForm />
|
||||
return (
|
||||
<>
|
||||
<AdminPermission />
|
||||
<CreateForm />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return <div>{dump(slug)}</div>
|
||||
|
||||
23
app/(protected)/admin/entity/[...slug]/page.tsx
Normal file
23
app/(protected)/admin/entity/[...slug]/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import AdminPermission from '@/components/(protected)/admin/auth/permission'
|
||||
import {EntityCrudForm} from '@/components/(protected)/admin/entity/crud-form'
|
||||
import {dump} from '@/lib/utils'
|
||||
|
||||
export default async function Page({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{slug?: string[]}>
|
||||
}) {
|
||||
const {slug} = await params
|
||||
|
||||
switch ((slug || [])[0]) {
|
||||
case 'create':
|
||||
return (
|
||||
<>
|
||||
<AdminPermission />
|
||||
<EntityCrudForm />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return <div>{dump(slug)}</div>
|
||||
}
|
||||
16
app/(protected)/admin/entity/page.tsx
Normal file
16
app/(protected)/admin/entity/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import AdminPermission from '@/components/(protected)/admin/auth/permission'
|
||||
|
||||
export default function AdminEntityPage() {
|
||||
return (
|
||||
<div>
|
||||
<AdminPermission />
|
||||
<p>
|
||||
<Link href='/admin/entity/create'>
|
||||
Створити блок / статтю / сторінку
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
30
app/(protected)/admin/order/page.tsx
Normal file
30
app/(protected)/admin/order/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import {Order} from '@prisma/client'
|
||||
import Link from 'next/link'
|
||||
|
||||
import {getAllOrders} from '@/actions/admin/order'
|
||||
import AdminPermission from '@/components/(protected)/admin/auth/permission'
|
||||
import {dump} from '@/lib/utils'
|
||||
|
||||
export default async function AdminOrderPage() {
|
||||
const orders = await getAllOrders()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminPermission />
|
||||
{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>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import AdminPermission from '@/components/(protected)/admin/auth/permission'
|
||||
import {EntityCrudForm} from '@/components/(protected)/admin/entity/crud-form'
|
||||
import ProductCreateEditForm from '@/components/(protected)/admin/product/create-edit-form'
|
||||
import {getProductById} from '@/lib/data/models/product'
|
||||
import {dump} from '@/lib/utils'
|
||||
@@ -21,9 +23,19 @@ export default async function Page({
|
||||
|
||||
switch (method) {
|
||||
case 'create':
|
||||
return <ProductCreateEditForm />
|
||||
return (
|
||||
<>
|
||||
<AdminPermission />
|
||||
<ProductCreateEditForm />
|
||||
</>
|
||||
)
|
||||
case 'update':
|
||||
return <ProductCreateEditForm data={data} />
|
||||
return (
|
||||
<>
|
||||
<AdminPermission />
|
||||
<ProductCreateEditForm data={data} />
|
||||
</>
|
||||
)
|
||||
default:
|
||||
return <div>{dump(slug)}</div>
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function AboutUsPage() {
|
||||
return (
|
||||
<div className='mb-12 mt-8'>
|
||||
<div className='container max-w-[922px] text-lg text-brand-violet-950'>
|
||||
<h1 className='text-3xl font-bold text-brand-violet'>
|
||||
Bewell: здоровий спосіб життя для всіх
|
||||
</h1>
|
||||
<div className='py-4'>
|
||||
<strong>Інтернет-магазин біологічних добавок Bewell</strong> — це
|
||||
зручна і надійна крамниця для всіх, хто піклується про здоров’я. У нас
|
||||
ви знайдете якісні дієтичні добавки від європейських виробників і
|
||||
зможете подбати про себе і про рідних без зайвих клопотів.
|
||||
</div>
|
||||
|
||||
<h2 className='my-4 text-2xl font-bold text-brand-violet'>
|
||||
Асортимент магазину <strong>Bewell</strong>
|
||||
</h2>
|
||||
<p>
|
||||
У нас є все, що потрібно для профілактики різноманітних захворювань та
|
||||
підтримки здоров’я. Це комплексні препарати, у складі яких
|
||||
переважають:
|
||||
</p>
|
||||
<ul className='my-4 ml-12 list-disc'>
|
||||
<li>екстракти рослин;</li>
|
||||
<li>мікро- та макроелементи;</li>
|
||||
<li>вітаміни.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Усі ці компоненти необхідно споживати щоденно, щоб отримати добову
|
||||
норму вітамінів, мікро- та макроелементів. Дієтичні добавки — просте
|
||||
доповнення до харчування, яке забезпечує організм необхідними
|
||||
поживними речовинами.
|
||||
</p>
|
||||
<p>
|
||||
Щоб визначитися, яка з добавок підійде саме вам, пропонуємо зануритися
|
||||
в наш каталог і вибрати відповідний розділ:
|
||||
</p>
|
||||
<ul className='my-4 ml-12 list-decimal'>
|
||||
<li>
|
||||
Комплексні препарати. Універсальні добавки для поліпшення здоров’я.
|
||||
Це вітаміни та мікроелементи для волосся, шкіри, підтримки
|
||||
імунітету, серця, нервової системи, травлення тощо.
|
||||
</li>
|
||||
<li>
|
||||
Добавки для підтримки жіночого здоров’я (
|
||||
<strong>вітаміни для жінок</strong>). Це зокрема препарати для
|
||||
відновлення менструального циклу, для полегшення симптомів
|
||||
менопаузи.
|
||||
</li>
|
||||
<li>
|
||||
Добавки для підтримки здоров’я чоловіків (
|
||||
<strong>вітаміни для чоловіків</strong>). Препарати для покращення
|
||||
статевої функції, здоров’я передміхурової залози.
|
||||
</li>
|
||||
<li>
|
||||
Препарати для зміцнення імунітету. Такі добавки корисні не лише для
|
||||
відновлення після захворювання та лікування, а також для
|
||||
профілактики захворювань.
|
||||
</li>
|
||||
<li>
|
||||
Добавки для відновлення енергії. Ці засоби допомагають боротися з
|
||||
втомою, покращують обмін речовин, допомагають почуватися енергійніше
|
||||
та краще спати.{' '}
|
||||
</li>
|
||||
</ul>
|
||||
<h2 className='my-4 text-2xl font-bold text-brand-violet'>
|
||||
Як замовити якісні дієтичні добавки?
|
||||
</h2>
|
||||
<p>
|
||||
В <strong>Bewell</strong> можна замовити продукцію відомих
|
||||
європейських виробників. Дієтичні добавки не є лікарськими засобами,
|
||||
але це хороша профілактика захворювань та підтримки здоров’я.
|
||||
Препарати, представлені в нашому магазині, можна побачити в
|
||||
асортименті аптек, адже це перевірена продукція, яка успішно
|
||||
використовується на лише в Україні, а й в Європі. Щоб зробити
|
||||
замовлення, виберіть потрібний препарат, додайте до кошика та зазначте
|
||||
умови відправки та оплати.
|
||||
</p>
|
||||
<h2 className='my-4 text-2xl font-bold text-brand-violet'>
|
||||
Bewell: наша філософія та принцип роботи
|
||||
</h2>
|
||||
<p>
|
||||
Головний пріоритет <strong>Bewell</strong> — підтримка здорового
|
||||
способу життя. Ми віримо, що ключ до гарного самопочуття та довголіття
|
||||
можна знайти в природі, збалансованому харчуванні та усвідомлений
|
||||
підтримці організму. Саме тому ми прагнемо допомогти кожному клієнту
|
||||
знайти найкращі добавки для підтримки організму та профілактики
|
||||
захворювань.
|
||||
</p>
|
||||
<p>
|
||||
Ми дбаємо про чесність і прозорість — пропонуємо лише сертифіковані,
|
||||
перевірені добавки, які сприяють зміцненню імунітету, відновленню
|
||||
енергії та покращенню сну, а також — внутрішній гармонії.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
114
app/[locale]/(root)/(shop)/cart/cart.tsx
Normal file
114
app/[locale]/(root)/(shop)/cart/cart.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
'use client'
|
||||
|
||||
import {Trash2} from 'lucide-react'
|
||||
import {useTranslations} from 'next-intl'
|
||||
import Image from 'next/image'
|
||||
import {useState} from 'react'
|
||||
|
||||
import CartPostSubmit from '@/app/[locale]/(root)/(shop)/cart/post-submit'
|
||||
import styles from '@/components/pages/cart/cart.module.scss'
|
||||
import CartItems from '@/components/pages/cart/items'
|
||||
import RegisteredOrderForm from '@/components/pages/cart/registered-order-form'
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'
|
||||
import {Link} from '@/i18n/routing'
|
||||
import {dump} from '@/lib/utils'
|
||||
import EmptyCartImage from '@/public/images/empty-cart.svg'
|
||||
import useCartStore from '@/store/cart-store'
|
||||
import {SessionUser} from '@/types/auth'
|
||||
import {Button} from '@/ui/button'
|
||||
|
||||
export default function Cart({user}: {user?: SessionUser | null}) {
|
||||
const t = useTranslations('cart')
|
||||
const [submitResult, setSubmitResult] = useState({})
|
||||
const {cartItems, clearCart} = useCartStore()
|
||||
const totalSum = cartItems.reduce(
|
||||
(total, product) => total + parseFloat(product.price) * product.quantity,
|
||||
0
|
||||
)
|
||||
|
||||
const resultSubmit = (result: any) => {
|
||||
setSubmitResult(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-1'>
|
||||
<div className='container'>
|
||||
<section className='bw-cart-wrapper mx-auto my-8 max-w-[640px] text-brand-violet'>
|
||||
{cartItems && cartItems.length > 0 ? (
|
||||
<>
|
||||
<div className='mb-6 flex items-center justify-between border-b border-b-brand-violet pb-2'>
|
||||
<h1 className='text-3xl font-bold'>{t('basket')}</h1>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
className='rounded-0 h-[3rem] w-[3rem] px-0 text-brand-violet'
|
||||
onClick={() => clearCart()}
|
||||
title={t('clear_cart')}
|
||||
>
|
||||
<Trash2 size={24} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<header className='flex text-xl'>
|
||||
<div className='col'>{t('title')}</div>
|
||||
<div className='flex-none'>{t('quantity')}</div>
|
||||
<div className='col text-right'>{t('amount')}</div>
|
||||
</header>
|
||||
<CartItems cartItems={cartItems} />
|
||||
|
||||
<footer className='my-8 flex py-4 text-xl'>
|
||||
<div className='col'></div>
|
||||
<div className='flex-none'>{t('total')}:</div>
|
||||
<div className='col text-right font-bold'>
|
||||
{totalSum.toFixed(2)} грн
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<section className={styles.bwOrderForm}>
|
||||
<h2 className='pb-9'>Оформлення замовлення</h2>
|
||||
<Tabs defaultValue='registered' className='w-full'>
|
||||
<TabsList className='grid w-full grid-cols-2'>
|
||||
<TabsTrigger value='registered'>
|
||||
Постійний клієнт
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value='quick-order'>
|
||||
Швидке замовлення
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='registered' className='mt-5'>
|
||||
<RegisteredOrderForm
|
||||
styles={styles.registeredForm}
|
||||
user={user}
|
||||
onSubmitHandler={resultSubmit}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value='quick-order'>quick-order</TabsContent>
|
||||
</Tabs>
|
||||
</section>
|
||||
</>
|
||||
) : Object.keys(submitResult).length === 0 ? (
|
||||
<div className='flex flex-col items-center justify-center gap-y-8'>
|
||||
<Image
|
||||
src={EmptyCartImage}
|
||||
sizes='88vw'
|
||||
alt={t('empty')}
|
||||
unoptimized={true}
|
||||
style={{
|
||||
width: '88%',
|
||||
height: 'auto',
|
||||
margin: '1rem auto'
|
||||
}}
|
||||
/>
|
||||
<Link href={'/catalog'} className='px-6 py-2'>
|
||||
<button className='rounded border border-brand-violet bg-transparent px-4 py-2 font-semibold text-brand-violet hover:border-transparent hover:bg-brand-yellow-300 hover:text-brand-violet-700'>
|
||||
{t('do_purchase')}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<CartPostSubmit result={submitResult} />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
236
app/[locale]/(root)/(shop)/cart/nova-post.tsx
Normal file
236
app/[locale]/(root)/(shop)/cart/nova-post.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from 'cmdk'
|
||||
import {
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
MapPinCheck,
|
||||
MapPinPlus,
|
||||
Warehouse
|
||||
} from 'lucide-react'
|
||||
import {useLocale, useTranslations} from 'next-intl'
|
||||
import {useState} from 'react'
|
||||
import {useDebouncedCallback} from 'use-debounce'
|
||||
|
||||
import {
|
||||
type Settlement,
|
||||
type Warehouse as WarehouseType,
|
||||
formatSettlement,
|
||||
getApi
|
||||
} from '@/lib/nova-post-helper'
|
||||
import {cn} from '@/lib/utils'
|
||||
import {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 NovaPost({onSelectHandler}: {onSelectHandler: any}) {
|
||||
const t = useTranslations('cart.post')
|
||||
const [citiesOpen, setCitiesOpen] = useState(false)
|
||||
const [warehousesOpen, setWarehousesOpen] = useState(false)
|
||||
const [citiesValue, setCitiesValue] = useState('')
|
||||
const [cityRef, setCityRef] = useState('')
|
||||
//const [warehouseRef, setWarehouseRef] = useState('')
|
||||
const [warehousesValue, setWarehousesValue] = useState('')
|
||||
const [cities, setCities] = useState([])
|
||||
const [warehouses, setWarehouses] = useState([])
|
||||
const locale = useLocale()
|
||||
|
||||
const handleCitySearch = useDebouncedCallback(
|
||||
async (e: string): Promise<void> => {
|
||||
if (e.length < 3) {
|
||||
setCities([])
|
||||
return
|
||||
}
|
||||
|
||||
const response = await getApi(url + `?scope=cities&q=` + encodeURI(e))
|
||||
|
||||
if (response.ok) {
|
||||
let json = await response.json()
|
||||
setCities(json)
|
||||
} else {
|
||||
setCities([])
|
||||
}
|
||||
},
|
||||
1000
|
||||
)
|
||||
|
||||
const handleWarehouseSearch = async (e: string): Promise<void> => {
|
||||
const response = await getApi(`${url}?scope=warehouses&q=${e}`)
|
||||
|
||||
if (response.ok) {
|
||||
let json = await response.json()
|
||||
setWarehouses(json)
|
||||
} else {
|
||||
setWarehouses([])
|
||||
}
|
||||
}
|
||||
|
||||
const cityDescription = (citiesValue: string): string => {
|
||||
const city: Settlement | undefined = cities.find(
|
||||
(city: Settlement) => city.Description === citiesValue
|
||||
)
|
||||
|
||||
if (!city) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return formatSettlement(city, locale)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-2'>
|
||||
<div>
|
||||
<Popover open={citiesOpen} onOpenChange={setCitiesOpen}>
|
||||
<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'>
|
||||
{citiesValue ? (
|
||||
<>
|
||||
<MapPinCheck />
|
||||
{cityDescription(citiesValue)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MapPinPlus />
|
||||
{t('findSettlement')}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<ChevronsUpDown className='opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-[640px] p-2'>
|
||||
<Command>
|
||||
<CommandInput
|
||||
className='border border-brand-violet p-2'
|
||||
placeholder={t('startSearchSettlement')}
|
||||
onValueChange={(e: string) => handleCitySearch(e)}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className='my-1'>{t('notFount')}</CommandEmpty>
|
||||
<CommandGroup className='max-h-[320px] w-full overflow-y-auto'>
|
||||
{cities.map((city: Settlement) => (
|
||||
<CommandItem
|
||||
className='my-2 flex'
|
||||
key={city?.Ref}
|
||||
value={city?.Description}
|
||||
onSelect={(currentValue: string) => {
|
||||
setCitiesValue(
|
||||
currentValue === citiesValue ? '' : currentValue
|
||||
)
|
||||
setCityRef(
|
||||
currentValue === citiesValue ? '' : city?.Ref
|
||||
)
|
||||
|
||||
handleWarehouseSearch(
|
||||
currentValue === citiesValue ? '' : city?.Ref
|
||||
).then(console.log)
|
||||
|
||||
setCitiesOpen(false)
|
||||
}}
|
||||
>
|
||||
{formatSettlement(city, locale)}
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
citiesValue === city?.Description
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
{cityRef !== '' && (
|
||||
<div className='pt-3'>
|
||||
<Popover open={warehousesOpen} onOpenChange={setWarehousesOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
/*aria-expanded={open}*/
|
||||
className='w-full justify-between'
|
||||
>
|
||||
<span className='inline-flex items-center gap-x-3'>
|
||||
<Warehouse />
|
||||
{warehousesValue ? warehousesValue : t('selectWarehouse')}
|
||||
</span>
|
||||
<ChevronsUpDown className='opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-[640px] p-2'>
|
||||
<Command>
|
||||
<CommandInput
|
||||
className='p-2'
|
||||
placeholder={t('startSearchWarehouse')}
|
||||
/*onValueChange={(e: string) => handleCitySearch(e)}*/
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className='my-1'>{t('notFount')}</CommandEmpty>
|
||||
<CommandGroup className='max-h-[320px] w-full overflow-y-auto'>
|
||||
{warehouses.map((warehouse: WarehouseType) => (
|
||||
<CommandItem
|
||||
className='my-2 flex'
|
||||
key={warehouse.Ref}
|
||||
value={warehouse.Description}
|
||||
onSelect={(currentValue: string) => {
|
||||
setWarehousesValue(
|
||||
currentValue === warehousesValue ? '' : currentValue
|
||||
)
|
||||
/*setWarehouseRef(
|
||||
currentValue === warehousesValue
|
||||
? ''
|
||||
: warehouse.Ref
|
||||
)*/
|
||||
onSelectHandler(
|
||||
currentValue === warehousesValue
|
||||
? {}
|
||||
: {
|
||||
Ref: warehouse.Ref,
|
||||
Description: warehouse.Description,
|
||||
DescriptionRu: warehouse.DescriptionRu
|
||||
}
|
||||
)
|
||||
setWarehousesOpen(false)
|
||||
}}
|
||||
>
|
||||
{warehouse.Description}
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
warehousesValue === warehouse.Description
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,46 +1,18 @@
|
||||
'use client'
|
||||
import Cart from '@/app/[locale]/(root)/(shop)/cart/cart'
|
||||
import {auth} from '@/auth'
|
||||
import {SessionUser} from '@/types/auth'
|
||||
|
||||
import {useTranslations} from 'next-intl'
|
||||
export default async function Page() {
|
||||
const session = await auth()
|
||||
if (!session) {
|
||||
return <Cart />
|
||||
}
|
||||
|
||||
import CartItems from '@/components/pages/cart/items'
|
||||
import useCartStore from '@/store/cart-store'
|
||||
const {user} = session
|
||||
|
||||
export default function Cart() {
|
||||
const t = useTranslations('cart')
|
||||
const {cartItems} = useCartStore()
|
||||
const totalSum = cartItems.reduce(
|
||||
(total, product) => total + parseFloat(product.price) * product.quantity,
|
||||
0
|
||||
)
|
||||
|
||||
// const subtotal = items.reduce(
|
||||
// (total, item) => total + item.price * item.quantity,
|
||||
// 0
|
||||
// )
|
||||
// const total = subtotal
|
||||
|
||||
return (
|
||||
<div className='mt-1'>
|
||||
<div className='container'>
|
||||
<section className='bw-cart-wrapper mx-auto my-8 max-w-[640px] text-brand-violet'>
|
||||
<h1 className='mb-6 border-b border-b-brand-violet pb-6 text-3xl font-bold'>
|
||||
{t('basket')}
|
||||
</h1>
|
||||
<header className='flex text-xl'>
|
||||
<div className='col'>{t('title')}</div>
|
||||
<div className='flex-none'>{t('quantity')}</div>
|
||||
<div className='col text-right'>{t('amount')}</div>
|
||||
</header>
|
||||
<CartItems />
|
||||
<footer className='my-8 flex border-y border-y-brand-violet py-4 text-xl'>
|
||||
<div className='col'></div>
|
||||
<div className='flex-none'>{t('total')}:</div>
|
||||
<div className='col text-right font-bold'>
|
||||
{totalSum.toFixed(2)} грн
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
return session ? (
|
||||
<Cart user={user as unknown as SessionUser} />
|
||||
) : (
|
||||
<Cart user={null} />
|
||||
)
|
||||
}
|
||||
|
||||
23
app/[locale]/(root)/(shop)/cart/post-submit.tsx
Normal file
23
app/[locale]/(root)/(shop)/cart/post-submit.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
export default function CartPostSubmit({result}: any) {
|
||||
if (result?.success) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className='text-2xl'>
|
||||
Номер Вашого замовлення:{' '}
|
||||
<span className='font-semibold text-brand-violet-950'>
|
||||
{result?.success}
|
||||
</span>{' '}
|
||||
</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className='text-2xl'>
|
||||
Сталася помилка:{' '}
|
||||
<span className='font-semibold text-red-800'>{result?.error}</span>{' '}
|
||||
</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
65
app/[locale]/(root)/(shop)/pages/[slug]/page.tsx
Normal file
65
app/[locale]/(root)/(shop)/pages/[slug]/page.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import {EntityLocale} from '@prisma/client'
|
||||
import type {Metadata} from 'next'
|
||||
import {notFound} from 'next/navigation'
|
||||
import {Suspense} from 'react'
|
||||
|
||||
import {getPageEntityBySlug} from '@/actions/admin/entity'
|
||||
import YoutubeComponent from '@/components/shared/youtube-component'
|
||||
import {dump, normalizeData, thisLocale} from '@/lib/utils'
|
||||
import {Skeleton} from '@/ui/skeleton'
|
||||
|
||||
type Props = {
|
||||
params: Promise<{slug?: string}>
|
||||
}
|
||||
|
||||
export const generateMetadata = async ({params}: Props): Promise<Metadata> => {
|
||||
const {slug} = await params
|
||||
const page = await getPageEntityBySlug(slug || '')
|
||||
if (!page) {
|
||||
notFound()
|
||||
}
|
||||
const {locales} = page
|
||||
const locale: EntityLocale = await thisLocale(locales)
|
||||
const {title, annotation} = locale
|
||||
return {
|
||||
title,
|
||||
description: normalizeData(annotation, {
|
||||
stripTags: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Pages({params}: Props) {
|
||||
const {slug} = await params
|
||||
const page = await getPageEntityBySlug(slug || '')
|
||||
|
||||
if (!page) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const {locales} = page
|
||||
const locale: EntityLocale = await thisLocale(locales)
|
||||
const {title, annotation, body} = locale
|
||||
|
||||
return (
|
||||
<div className='mb-12 mt-8'>
|
||||
<div className='bw-page container max-w-[800px] text-lg text-brand-violet-950'>
|
||||
<h1>{title}</h1>
|
||||
<section className='min-h-[450px]'>
|
||||
<Suspense fallback={<Skeleton className='h-full w-full' />}>
|
||||
<YoutubeComponent id='qfg2UlQk__M' />
|
||||
</Suspense>
|
||||
</section>
|
||||
|
||||
<article
|
||||
className='mt-6'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: ((annotation ?? '') + body) as string
|
||||
}}
|
||||
></article>
|
||||
|
||||
{/*{dump(locale)}*/}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import {getProductByIdWitData} from '@prisma/client/sql'
|
||||
import {notFound} from 'next/navigation'
|
||||
|
||||
import ProductPageIndex from '@/components/pages/product'
|
||||
import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas'
|
||||
import {db} from '@/lib/db/prisma/client'
|
||||
|
||||
export default async function Products({
|
||||
params
|
||||
@@ -12,5 +15,9 @@ export default async function Products({
|
||||
const id = (uri || '').match(/^(\d+)-./)
|
||||
if (!id) notFound()
|
||||
|
||||
return <ProductPageIndex id={id[1]} />
|
||||
const data: CategoryPageSqlSchema[] = await db.$queryRawTyped(
|
||||
getProductByIdWitData(id[1])
|
||||
)
|
||||
|
||||
return <ProductPageIndex data={data} id={id[1]} />
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {getCatalogIndexData} from '@prisma/client/sql'
|
||||
import {getLocale} from 'next-intl/server'
|
||||
import Image from 'next/image'
|
||||
import React from 'react'
|
||||
|
||||
import FeatureCards from '@/components/shared/home/feature-cards'
|
||||
import {HomeCarousel} from '@/components/shared/home/home-carousel'
|
||||
import AppCatalog from '@/components/shared/sidebar/app-catalog'
|
||||
import FeatureCardFront from '@/components/shared/store/feature-card-front'
|
||||
import Terms from '@/components/shared/terms'
|
||||
import {carousels} from '@/lib/data'
|
||||
import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas'
|
||||
import {db} from '@/lib/db/prisma/client'
|
||||
@@ -31,9 +33,9 @@ import image from '@/public/uploads/products/IMG_6572.jpg'
|
||||
// }
|
||||
|
||||
export default async function HomePage() {
|
||||
const loc = await getLocale()
|
||||
const locale = await getLocale()
|
||||
const catalog: CategoryPageSqlSchema[] = await db.$queryRawTyped(
|
||||
getCatalogIndexData(loc)
|
||||
getCatalogIndexData(locale)
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -51,8 +53,18 @@ export default async function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className='container mb-4 mt-[128px]'>
|
||||
<section className='container mb-4 mt-8'>
|
||||
<FeatureCards items={catalog} />
|
||||
</section>
|
||||
|
||||
<Terms />
|
||||
|
||||
<section className='container mb-4 mt-12'>
|
||||
<h2 className='font-heading text-center text-3xl font-bold uppercase tracking-tight text-brand-violet'>
|
||||
{locale !== 'ru' ? "Цікаво про здоров'я" : 'Интересно о здоровье'}
|
||||
</h2>
|
||||
</section>
|
||||
<section className='container mb-4 mt-8'>
|
||||
<div className='re relative my-12 overflow-hidden'>
|
||||
<Image
|
||||
alt={''}
|
||||
|
||||
150
app/api/nova-post/route.ts
Normal file
150
app/api/nova-post/route.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
'use server'
|
||||
|
||||
import {NextRequest} from 'next/server'
|
||||
import {json} from 'node:stream/consumers'
|
||||
|
||||
import {type Warehouse} from '@/lib/nova-post-helper'
|
||||
|
||||
// , res: Response
|
||||
export async function GET(req: NextRequest) {
|
||||
const searchParams = req.nextUrl.searchParams
|
||||
const scope = searchParams.get('scope')
|
||||
|
||||
let response: any = []
|
||||
switch (scope) {
|
||||
case 'cities':
|
||||
response = await getCities(searchParams.get('q') || '...')
|
||||
break
|
||||
case 'warehouses':
|
||||
response = await getWarehouses(searchParams.get('q') || '...')
|
||||
break
|
||||
case 'warehouse':
|
||||
response = await getWarehouse(searchParams.get('q') || '...')
|
||||
break
|
||||
case 'streets':
|
||||
response = await getStreet(searchParams.get('q') || '...')
|
||||
break
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify(response.success ? response.data : [], null, 2),
|
||||
{
|
||||
status: 200,
|
||||
headers: {'Content-Type': 'application/json; charset=utf-8'}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchApi(init: RequestInit): Promise<Response> {
|
||||
return await fetch(process.env.NOVA_POST_API_EP || '', init)
|
||||
}
|
||||
|
||||
async function getWarehouses(CityRef: string, Page: number = 1) {
|
||||
// const branches = []
|
||||
let c = 0
|
||||
// let n = 0
|
||||
const Limit = 500
|
||||
|
||||
/*do {
|
||||
const response = await fetchApi({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
apiKey: process.env.NOVA_POST_API_KEY || '',
|
||||
modelName: 'AddressGeneral',
|
||||
calledMethod: 'getWarehouses',
|
||||
methodProperties: {
|
||||
CityRef,
|
||||
Page: ++c,
|
||||
Limit,
|
||||
FindByString: 'відд'
|
||||
}
|
||||
})
|
||||
})
|
||||
list = await response.json()
|
||||
n = Math.ceil(list.info.totalCount / Limit)
|
||||
|
||||
for (const i in list.data) {
|
||||
if (list.data[i].Description.trim().match(/^відді/iu)) {
|
||||
branches.push(list.data[i])
|
||||
}
|
||||
}
|
||||
} while (c < n)
|
||||
|
||||
list.data = branches*/
|
||||
|
||||
const response = await fetchApi({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
apiKey: process.env.NOVA_POST_API_KEY || '',
|
||||
modelName: 'AddressGeneral',
|
||||
calledMethod: 'getWarehouses',
|
||||
methodProperties: {
|
||||
CityRef,
|
||||
Page: ++c,
|
||||
Limit,
|
||||
FindByString: 'відд'
|
||||
}
|
||||
})
|
||||
})
|
||||
const list = await response.json()
|
||||
|
||||
list.data = list.data.filter((item: Warehouse) =>
|
||||
item.Description.trim().match(/^відді/iu)
|
||||
)
|
||||
//console.log(Math.ceil(list.info.totalCount / Limit), list.data.length)
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
async function getWarehouse(Ref: string) {
|
||||
const response = await fetch(process.env.NOVA_POST_API_EP || '', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
apiKey: process.env.NOVA_POST_API_KEY || '',
|
||||
modelName: 'AddressGeneral',
|
||||
calledMethod: 'getWarehouses',
|
||||
methodProperties: {
|
||||
Ref,
|
||||
Limit: 1
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
async function getCities(searchString: string) {
|
||||
const response = await fetch(process.env.NOVA_POST_API_EP || '', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
apiKey: process.env.NOVA_POST_API_KEY || '',
|
||||
modelName: 'AddressGeneral',
|
||||
calledMethod: 'getCities',
|
||||
methodProperties: {
|
||||
FindByString: searchString,
|
||||
Limit: 500
|
||||
}
|
||||
})
|
||||
})
|
||||
//console.log(searchString)
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
async function getStreet(StreetName: string) {
|
||||
const response = await fetch(process.env.NOVA_POST_API_EP || '', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
apiKey: process.env.NOVA_POST_API_KEY || '',
|
||||
modelName: 'AddressGeneral',
|
||||
calledMethod: 'searchSettlementStreets',
|
||||
methodProperties: {
|
||||
SettlementRef: 'e718a680-4b33-11e4-ab6d-005056801329',
|
||||
Page: 1,
|
||||
Limit: 50,
|
||||
StreetName
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
126
app/globals.css
126
app/globals.css
@@ -4,10 +4,8 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: scroll;
|
||||
html, body {
|
||||
@apply h-full m-0 p-0;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -115,11 +113,11 @@ body {
|
||||
}
|
||||
|
||||
.bw-layout-col-left {
|
||||
@apply flex-1 sm:w-7/12 md:w-5/12 xl:w-4/12 lg:flex-col
|
||||
@apply flex-1 sm:w-7/12 md:w-5/12 xl:w-5/12 lg:flex-col
|
||||
}
|
||||
|
||||
.bw-layout-col-right {
|
||||
@apply sm:w-5/12 md:w-7/12 xl:w-8/12 flex-1 sm:flex-auto sm:pl-4 md:pl-7 xl:pl-9
|
||||
@apply sm:w-5/12 md:w-7/12 xl:w-7/12 flex-1 sm:flex-auto sm:pl-4 md:pl-7 xl:pl-9
|
||||
}
|
||||
|
||||
.bw-product-col-left{
|
||||
@@ -134,7 +132,7 @@ body {
|
||||
}
|
||||
|
||||
.bw-header-col-right {
|
||||
@apply flex-grow-0 flex-shrink-0 md:basis-[272px]
|
||||
@apply flex-grow-0 flex-shrink-0 md:basis-[142px]
|
||||
}
|
||||
|
||||
.bw-border-color {
|
||||
@@ -146,6 +144,76 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.bw-terms-section {
|
||||
|
||||
.bw-accordion-item {
|
||||
|
||||
@apply w-[100%] flex-none overflow-hidden border-b-0 md:w-[46.75%] lg:w-[29.25%]; /*shadow-md*/
|
||||
|
||||
/*&:hover {
|
||||
@apply bg-brand-violet-800/25;
|
||||
}*/
|
||||
|
||||
&[data-state="open"]{
|
||||
@apply border-brand-violet-800/25 border-[2px] rounded-lg; /*shadow-md shadow-brand-violet-900/30*/
|
||||
|
||||
.bw-accordion-content {
|
||||
@apply rounded-br-lg rounded-bl-lg bg-brand-violet-50/15; /*border-r-[2px] border-b-[2px] border-l-[2px]*/
|
||||
}/*bg-brand-violet-50/50*/
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
&[data-state="closed"] {
|
||||
@apply border-[2px] rounded-lg; /*shadow-lg border-brand-violet-900/10*/
|
||||
}
|
||||
|
||||
&[data-state="open"] {
|
||||
@apply border-[2px] rounded-none py-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bw-accordion-trigger {
|
||||
@apply text-center text-base text-brand-violet antialiased font-semibold font-heading;
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
padding: 10px 16px;
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
|
||||
&:hover, &[data-state="open"] {
|
||||
@apply bg-brand-violet-800/25 text-white border-brand-violet-800/5; /*border-t-2*/
|
||||
}
|
||||
}
|
||||
svg {
|
||||
@apply w-6 h-6;
|
||||
}
|
||||
|
||||
h3[data-state="open"], h3:hover {
|
||||
& > button > svg {
|
||||
@apply stroke-white;
|
||||
}
|
||||
}
|
||||
|
||||
.bw-accordion-content {
|
||||
@apply px-4 text-[15px] leading-relaxed tracking-wide text-brand-violet-900;
|
||||
|
||||
p {
|
||||
@apply block my-[1em] mx-0;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply font-semibold text-brand-violet-600 hover:underline;
|
||||
}
|
||||
|
||||
ul {
|
||||
@apply block list-disc my-[1em] m-0 pl-10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bw-dd-menu {
|
||||
/* since nested groupes are not supported we have to use
|
||||
regular css for the nested dropdowns
|
||||
@@ -165,6 +233,40 @@ body {
|
||||
.group:hover .group-hover\:-rotate-180 { transform: rotate(180deg) }
|
||||
}
|
||||
|
||||
.bw-page {
|
||||
@apply leading-relaxed tracking-tight mt-10;
|
||||
|
||||
h1 {
|
||||
@apply font-heading my-[0.67em] mx-0 text-3xl font-bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply font-heading mt-[0.83em] mb-[0.25em] mx-0 text-xl font-semibold;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply block mb-[1em] mx-0;
|
||||
}
|
||||
|
||||
/*a {
|
||||
@apply font-semibold text-brand-violet-600 hover:underline;
|
||||
}*/
|
||||
|
||||
ul {
|
||||
@apply block list-disc my-[1em] m-0 pl-10;
|
||||
}
|
||||
|
||||
article {
|
||||
@apply leading-relaxed;
|
||||
}
|
||||
/*display: block;
|
||||
font-size: 2em;
|
||||
margin-top: 0.67em;
|
||||
margin-bottom: 0.67em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
font-weight: bold;*/
|
||||
}
|
||||
|
||||
.jodit-wysiwyg > * {
|
||||
all: revert;
|
||||
@@ -180,6 +282,7 @@ body {
|
||||
}*/
|
||||
}
|
||||
|
||||
|
||||
#admin-bw-panel form {
|
||||
input {
|
||||
@apply bg-white outline-0 text-[16px] leading-none;
|
||||
@@ -265,3 +368,12 @@ body {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
https://www.w3schools.com/cssref/css_default_values.php*/
|
||||
|
||||
.bw-yt-video {
|
||||
@apply aspect-video w-full self-stretch md:min-h-96;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {Metadata} from 'next'
|
||||
import localFont from 'next/font/local'
|
||||
import {headers} from 'next/headers'
|
||||
import {ReactNode} from 'react'
|
||||
import {Toaster} from 'react-hot-toast'
|
||||
@@ -16,6 +17,42 @@ export const metadata: Metadata = {
|
||||
description: APP_DESCRIPTION
|
||||
}
|
||||
|
||||
const Myriad = localFont({
|
||||
variable: '--font-myriad',
|
||||
src: [
|
||||
/*{
|
||||
path: '../public/fonts/myriad-light.woff2',
|
||||
weight: '300',
|
||||
style: 'normal'
|
||||
},*/
|
||||
{
|
||||
path: '../public/fonts/myriad-regular.woff2',
|
||||
weight: '400',
|
||||
style: 'normal'
|
||||
},
|
||||
/*{
|
||||
path: '../public/fonts/myriad-it.woff2',
|
||||
weight: '400',
|
||||
style: 'italic'
|
||||
},*/
|
||||
{
|
||||
path: '../public/fonts/myriad-semibold.woff2',
|
||||
weight: '600',
|
||||
style: 'normal'
|
||||
},
|
||||
{
|
||||
path: '../public/fonts/myriad-bold.woff2',
|
||||
weight: '700',
|
||||
style: 'normal'
|
||||
}
|
||||
/*{
|
||||
path: '../public/fonts/myriad-boldit.woff2',
|
||||
weight: '700',
|
||||
style: 'italic'
|
||||
}*/
|
||||
]
|
||||
})
|
||||
|
||||
export default async function RootLayout({
|
||||
children
|
||||
}: Readonly<{children: ReactNode}>) {
|
||||
@@ -23,8 +60,8 @@ export default async function RootLayout({
|
||||
const locale = headersList.get('x-site-locale') ?? routing.defaultLocale
|
||||
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<body className='min-h-screen antialiased'>
|
||||
<html lang={locale} suppressHydrationWarning className={Myriad.variable}>
|
||||
<body className={`min-h-screen antialiased`}>
|
||||
{children}
|
||||
{/*<Toaster />*/}
|
||||
<Toaster position='top-right' reverseOrder={false} />
|
||||
|
||||
Reference in New Issue
Block a user