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

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

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

View File

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

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

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

View File

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

View File

@@ -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={''}