grand commit
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
// import styles from '@/components/pages/cart/cart.module.scss'
|
||||
import Link from 'next/link'
|
||||
import {Minus, Plus} from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import useCartStore from '@/store/cart-store'
|
||||
import {Link} from '@/i18n/routing'
|
||||
import useCartStore, {CartItem} from '@/store/cart-store'
|
||||
import {Button} from '@/ui/button'
|
||||
import {Input} from '@/ui/input'
|
||||
|
||||
export default function CartItems() {
|
||||
const {cartItems} = useCartStore()
|
||||
@@ -27,7 +28,7 @@ export default function CartItems() {
|
||||
<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={'/products'}
|
||||
href={'/catalog'}
|
||||
className='rounded-md bg-orange-500 px-6 py-2 text-white'
|
||||
>
|
||||
Shop
|
||||
@@ -38,33 +39,45 @@ export default function CartItems() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{cartItems?.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className='bsdg-brand-violet-200 bw-cart-item-counter my-4 flex items-center gap-4 py-4'
|
||||
>
|
||||
<div className='flex-auto bg-brand-violet-100'>{item.title}</div>
|
||||
<div className='flex w-16 flex-none items-center justify-center'>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className='rounded-0'
|
||||
onClick={() => onDecreaseQuantity(item.id)}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
<div className='mx-4 border-0 text-xl font-bold leading-none text-brand-violet'>
|
||||
{item.quantity}
|
||||
</div>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className='rounded-0'
|
||||
onClick={() => onIncreaseQuantity(item.id)}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
{cartItems?.map((item: CartItem, i: number) => (
|
||||
<div className='my-4 flex items-center' key={i}>
|
||||
<div className='col'>
|
||||
{item.title}
|
||||
<Image
|
||||
src={(item?.image || '').replace('.jpg', '-thumb.jpg')}
|
||||
alt=''
|
||||
width={96}
|
||||
height={96}
|
||||
className='rounded-md border'
|
||||
style={{
|
||||
width: '96px',
|
||||
height: '96px',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-3 flex-auto text-right font-bold'>
|
||||
<div className='flex-none'>
|
||||
<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'
|
||||
onClick={() => onDecreaseQuantity(item.id)}
|
||||
>
|
||||
<Minus />
|
||||
</Button>
|
||||
<div className='mx-4 text-xl font-bold leading-none text-brand-violet'>
|
||||
{item.quantity}
|
||||
</div>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className='rounded-0 h-[3rem] w-[3rem] text-2xl leading-none text-brand-violet'
|
||||
onClick={() => onIncreaseQuantity(item.id)}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='col text-right text-lg font-bold'>
|
||||
{(item.price * item.quantity).toFixed(2)} грн
|
||||
</div>
|
||||
</div>
|
||||
|
||||
29
components/pages/category/page.tsx
Normal file
29
components/pages/category/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import AppCatalog from '@/components/shared/sidebar/app-catalog'
|
||||
import FeatureCard from '@/components/shared/store/feature-card'
|
||||
import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas'
|
||||
import {thisLocales} from '@/lib/utils'
|
||||
|
||||
export default async function CategoryPageIndex({
|
||||
data
|
||||
}: {
|
||||
data: CategoryPageSqlSchema[]
|
||||
}) {
|
||||
const locales = await thisLocales(data)
|
||||
|
||||
return (
|
||||
<div className='mt-1'>
|
||||
<div className='container flex flex-col sm:flex-row'>
|
||||
<section className='bw-layout-col-left pt-3'>
|
||||
<AppCatalog />
|
||||
</section>
|
||||
<div className='bw-layout-col-right pt-3'>
|
||||
<section className='grid w-full grid-cols-3 gap-6 p-6 shadow-xl shadow-brand-violet/25'>
|
||||
{locales.map((card: CategoryPageSqlSchema, i: number) => (
|
||||
<FeatureCard key={i} card={card} />
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import {ShoppingCartIcon} from 'lucide-react'
|
||||
import {useTranslations} from 'next-intl'
|
||||
|
||||
import {dump} from '@/lib/utils'
|
||||
import useCartStore, {CartItem} from '@/store/cart-store'
|
||||
|
||||
export default function AddCartButton({product}: {product: CartItem}) {
|
||||
const t = useTranslations('cart')
|
||||
const addItemToCart = useCartStore(state => state.addItemToCart)
|
||||
const {cartItems} = useCartStore(state => state)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className='flex flex-col items-center'
|
||||
role='button'
|
||||
title={t('basket')}
|
||||
onClick={() => addItemToCart(product)}
|
||||
>
|
||||
<ShoppingCartIcon className='h-[21px] w-[21px]' />
|
||||
</button>
|
||||
|
||||
<pre>{dump(cartItems)}</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
116
components/pages/product/carousel.tsx
Normal file
116
components/pages/product/carousel.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import {ProductResource} from '@prisma/client'
|
||||
import {EmblaOptionsType} from 'embla-carousel'
|
||||
import useEmblaCarousel from 'embla-carousel-react'
|
||||
import Image from 'next/image'
|
||||
import React, {useCallback, useEffect, useState} from 'react'
|
||||
|
||||
import {Thumb} from './thumb'
|
||||
import {Dialog, DialogContent, DialogTitle} from '@/ui/dialog'
|
||||
|
||||
type PropType = {
|
||||
slides: number[]
|
||||
options?: EmblaOptionsType
|
||||
}
|
||||
|
||||
export default function ProductCarousel({
|
||||
images,
|
||||
title
|
||||
}: {
|
||||
images: ProductResource[] | null
|
||||
title: string
|
||||
}) {
|
||||
//const {slides, options} = props
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const [emblaMainRef, emblaMainApi] = useEmblaCarousel({}) //options
|
||||
const [emblaThumbsRef, emblaThumbsApi] = useEmblaCarousel({
|
||||
containScroll: 'keepSnaps',
|
||||
dragFree: true
|
||||
})
|
||||
|
||||
const onThumbClick = useCallback(
|
||||
(index: number) => {
|
||||
if (!emblaMainApi || !emblaThumbsApi) return
|
||||
emblaMainApi.scrollTo(index)
|
||||
},
|
||||
[emblaMainApi, emblaThumbsApi]
|
||||
)
|
||||
|
||||
const onSelect = useCallback(() => {
|
||||
if (!emblaMainApi || !emblaThumbsApi) return
|
||||
setSelectedIndex(emblaMainApi.selectedScrollSnap())
|
||||
emblaThumbsApi.scrollTo(emblaMainApi.selectedScrollSnap())
|
||||
}, [emblaMainApi, emblaThumbsApi, setSelectedIndex])
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaMainApi) return
|
||||
onSelect()
|
||||
|
||||
emblaMainApi.on('select', onSelect).on('reInit', onSelect)
|
||||
}, [emblaMainApi, onSelect])
|
||||
//let featuredImage: ProductResource | null | undefined
|
||||
/*if ((resources || []).length > 0) {
|
||||
featuredImage = resources?.find(resource => resource.isFeature)
|
||||
}*/
|
||||
|
||||
return (
|
||||
<div className='embla my-8'>
|
||||
<div className='embla__viewport' ref={emblaMainRef}>
|
||||
<div className='embla__container'>
|
||||
{images?.map((image: ProductResource, index: number) => (
|
||||
<div className='embla__slide' key={index}>
|
||||
<div className='embla__slide__number h-[480px] overflow-hidden'>
|
||||
<Image
|
||||
src={image.uri}
|
||||
alt=''
|
||||
width={image.width || 100}
|
||||
height={image.height || 100}
|
||||
sizes='100vw'
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
maxHeight: '480px',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='embla-thumbs mb-8 mt-6'>
|
||||
<div className='embla-thumbs__viewport' ref={emblaThumbsRef}>
|
||||
<div className='embla-thumbs__container flex items-center justify-center gap-x-6'>
|
||||
{images?.map((image: ProductResource, index: number) => (
|
||||
<Dialog key={index}>
|
||||
<DialogTitle className='hidden'>{title}</DialogTitle>
|
||||
<Thumb
|
||||
image={image}
|
||||
onClick={() => onThumbClick(index)}
|
||||
selected={index === selectedIndex}
|
||||
index={index}
|
||||
/>
|
||||
<DialogContent className='overflow w-full'>
|
||||
<Image
|
||||
src={image.uri}
|
||||
alt=''
|
||||
width={image.width || 100}
|
||||
height={image.height || 100}
|
||||
sizes='100vw'
|
||||
style={{
|
||||
width: '100%',
|
||||
maxHeight: '100dvh',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
81
components/pages/product/embla.css
Normal file
81
components/pages/product/embla.css
Normal file
@@ -0,0 +1,81 @@
|
||||
.embla {
|
||||
max-width: 48rem;
|
||||
margin: auto;
|
||||
--slide-height: 19rem;
|
||||
--slide-spacing: 1rem;
|
||||
--slide-size: 100%;
|
||||
}
|
||||
.embla__viewport {
|
||||
overflow: hidden;
|
||||
}
|
||||
.embla__container {
|
||||
display: flex;
|
||||
touch-action: pan-y pinch-zoom;
|
||||
margin-left: calc(var(--slide-spacing) * -1);
|
||||
}
|
||||
.embla__slide {
|
||||
transform: translate3d(0, 0, 0);
|
||||
flex: 0 0 var(--slide-size);
|
||||
min-width: 0;
|
||||
padding-left: var(--slide-spacing);
|
||||
}
|
||||
.embla__slide__number {
|
||||
box-shadow: inset 0 0 0 0.2rem var(--detail-medium-contrast);
|
||||
border-radius: 1.8rem;
|
||||
font-size: 4rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--slide-height);
|
||||
user-select: none;
|
||||
}
|
||||
.embla-thumbs {
|
||||
--thumbs-slide-spacing: 0.8rem;
|
||||
--thumbs-slide-height: 6rem;
|
||||
margin-top: var(--thumbs-slide-spacing);
|
||||
}
|
||||
.embla-thumbs__viewport {
|
||||
overflow: hidden;
|
||||
}
|
||||
.embla-thumbs__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: calc(var(--thumbs-slide-spacing) * -1);
|
||||
}
|
||||
.embla-thumbs__slide {
|
||||
flex: 0 0 22%;
|
||||
min-width: 0;
|
||||
padding-left: var(--thumbs-slide-spacing);
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.embla-thumbs__slide {
|
||||
flex: 0 0 15%;
|
||||
}
|
||||
}
|
||||
.embla-thumbs__slide__number {
|
||||
border-radius: 1.8rem;
|
||||
-webkit-tap-highlight-color: rgba(var(--text-high-contrast-rgb-value), 0.5);
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
touch-action: manipulation;
|
||||
display: inline-flex;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-shadow: inset 0 0 0 0.2rem var(--detail-medium-contrast);
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--detail-high-contrast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--thumbs-slide-height);
|
||||
width: 100%;
|
||||
}
|
||||
.embla-thumbs__slide--selected .embla-thumbs__slide__number {
|
||||
color: var(--text-body);
|
||||
}
|
||||
@@ -1,37 +1,79 @@
|
||||
import {getLocale} from 'next-intl/server'
|
||||
import {ProductResource} from '@prisma/client'
|
||||
import {getProductByIdWitData} from '@prisma/client/sql'
|
||||
import {getTranslations} from 'next-intl/server'
|
||||
import {notFound} from 'next/navigation'
|
||||
import {strict} from 'node:assert'
|
||||
|
||||
import AddCartButton from '@/components/pages/product/add-cart-button'
|
||||
import {ProductProps, getProductById} from '@/lib/data/models/product'
|
||||
import {dump} from '@/lib/utils'
|
||||
import useCartStore from '@/store/cart-store'
|
||||
import ProductCarousel from '@/components/pages/product/carousel'
|
||||
import AddCartButton from '@/components/shared/store/add-cart-button'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from '@/components/ui/breadcrumb'
|
||||
import {getProductResources} from '@/lib/data/models/product'
|
||||
import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas'
|
||||
import {db} from '@/lib/db/prisma/client'
|
||||
import {thisLocale, toPrice} from '@/lib/utils'
|
||||
import {Separator} from '@/ui/separator'
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/ui/tabs'
|
||||
|
||||
export default async function ProductPageIndex({id}: {id: string}) {
|
||||
const product = await getProductById(parseInt(id))
|
||||
if (!product) notFound()
|
||||
const {locales, toStore} = product as ProductProps
|
||||
const lang = await getLocale()
|
||||
const locale = locales[lang === 'uk' ? 0 : 1]
|
||||
const store = JSON.parse(JSON.stringify(toStore[0]))
|
||||
const t = await getTranslations('Common')
|
||||
|
||||
const data: CategoryPageSqlSchema[] = await db.$queryRawTyped(
|
||||
getProductByIdWitData(id)
|
||||
)
|
||||
|
||||
const locale = await thisLocale(data)
|
||||
if (!locale) notFound()
|
||||
|
||||
const resources: ProductResource[] | null = await getProductResources(
|
||||
parseInt(id)
|
||||
)
|
||||
|
||||
//const files = await getMetaOfFile(locale.productId)
|
||||
|
||||
return (
|
||||
<div className='mt-1'>
|
||||
<div className='container flex flex-col sm:flex-row'>
|
||||
<div>
|
||||
<AddCartButton
|
||||
product={{
|
||||
id: product.id,
|
||||
quantity: 1,
|
||||
title: locale.title,
|
||||
price: store.price as string, //parseFloat().toFixed(2),
|
||||
image: product.image
|
||||
}}
|
||||
/>
|
||||
<hr />
|
||||
</div>
|
||||
<div>
|
||||
<div className='container flex flex-col gap-8 sm:flex-row'>
|
||||
{/*<pre>{dump(resources)}</pre>*/}
|
||||
<div className='bw-product-col-left pt-3'>
|
||||
<Breadcrumb className='mb-4'>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href={'/'}>{t('home')}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href={`/category/${locale.categorySlug}`}>
|
||||
{locale.categoryTitle}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{locale.title}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className='flex w-[82%] items-center justify-between'>
|
||||
<h1 className='my-4 text-3xl font-bold text-brand-violet-950'>
|
||||
{locale.title}
|
||||
</h1>
|
||||
<AddCartButton
|
||||
product={{
|
||||
id: locale.productId,
|
||||
quantity: 1,
|
||||
title: locale.title,
|
||||
price: toPrice(locale.price),
|
||||
image: locale.image
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Separator className='my-4 w-[82%] border-b border-brand-violet' />
|
||||
<ProductCarousel images={resources} title={locale.title} />
|
||||
<Tabs defaultValue='article' className=''>
|
||||
<TabsList className='grid w-full grid-cols-2'>
|
||||
<TabsTrigger value='article'>Опис</TabsTrigger>
|
||||
@@ -51,6 +93,14 @@ export default async function ProductPageIndex({id}: {id: string}) {
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<section className='bw-product-col-right bg-gray-50 pt-3'>
|
||||
<div className=''>
|
||||
{t('price')}:
|
||||
<span className='ml-2 flex-auto text-right text-xl font-bold text-brand-violet'>
|
||||
{toPrice(locale.price)}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
46
components/pages/product/thumb.tsx
Normal file
46
components/pages/product/thumb.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import {ProductResource} from '@prisma/client'
|
||||
import Image from 'next/image'
|
||||
import React from 'react'
|
||||
|
||||
import {Button} from '@/ui/button'
|
||||
import {DialogTrigger} from '@/ui/dialog'
|
||||
|
||||
type PropType = {
|
||||
selected: boolean
|
||||
index: number
|
||||
onClick: () => void
|
||||
image: ProductResource
|
||||
}
|
||||
|
||||
export const Thumb: React.FC<PropType> = props => {
|
||||
const {selected, index, onClick, image} = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'embla-thumbs__slide'.concat(
|
||||
selected ? 'embla-thumbs__slide--selected' : ''
|
||||
)}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
onClick={onClick}
|
||||
type='button'
|
||||
className='embla-thumbs__slide__number'
|
||||
>
|
||||
<Image
|
||||
src={image.uri.replace('.jpg', '-thumb.jpg')}
|
||||
alt=''
|
||||
width={96}
|
||||
height={96}
|
||||
className='rounded-md border'
|
||||
style={{
|
||||
width: '96px',
|
||||
height: '96px',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user