grand commit

This commit is contained in:
2025-02-07 08:34:42 +02:00
parent f594f001f6
commit c6c34f0453
53 changed files with 1283 additions and 625 deletions

View File

@@ -4,15 +4,14 @@ import {zodResolver} from '@hookform/resolvers/zod'
import dynamic from 'next/dynamic'
import React, {useEffect, useMemo, useRef, useState} from 'react'
import {useFieldArray, useForm} from 'react-hook-form'
import toast from 'react-hot-toast'
import {z} from 'zod'
import {onProductCreateAction} from '@/actions/admin/product'
import {useToast} from '@/hooks/use-toast'
import {i18nDefaultLocale, i18nLocales} from '@/i18n-config'
import {BaseEditorConfig} from '@/lib/config/editor'
import {createProductFormSchema} from '@/lib/schemas/admin/product'
import {toEmptyParams} from '@/lib/utils'
import useCountStore from '@/store/cart-store'
import {Button} from '@/ui/button'
import {
Form,
@@ -64,7 +63,6 @@ export default function ProductCreateEditForm({data}: {data?: any}) {
data?.locales[1].instruction || ''
)
const editor = useRef(null) //declared a null value
const {toast} = useToast()
const config = useMemo(() => BaseEditorConfig, [])
@@ -129,20 +127,12 @@ export default function ProductCreateEditForm({data}: {data?: any}) {
setError(res?.error)
setSuccess('')
setLoading(false)
toast({
variant: 'destructive',
title: res?.error,
description: res?.message
})
toast.error(res?.error)
} else {
setSuccess(res?.success as string)
setError('')
setLoading(false)
toast({
variant: 'success',
title: res?.success,
description: res?.message
})
toast.success(res?.success)
}
})
}

View File

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

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

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

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

View File

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

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

View File

@@ -15,13 +15,6 @@ type MenuProps = {
}
export default async function Header() {
/*{
searchParams
}: {
searchParams: Promise<{query?: string}>
}*/
//const query = (await searchParams).query
return (
<header className='w-full border-none bg-background text-white'>
<div className='container flex'>

View File

@@ -1,34 +1,22 @@
import Image from 'next/image'
import * as React from 'react'
import {cards} from '@/lib/data'
import {Card, CardContent} from '@/ui/card'
import FeatureCardFront from '@/components/shared/store/feature-card-front'
import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas'
import {Carousel, CarouselContent, CarouselItem} from '@/ui/carousel'
export default function FeatureCards() {
export default function FeatureCards({
items
}: {
items: CategoryPageSqlSchema[]
}) {
return (
<Carousel className='mx-auto w-full'>
<CarouselContent className='-ml-2 md:-ml-4'>
{cards.map((card: any) => (
{items.map((card: any) => (
<CarouselItem
key={card.title}
className='pl-3 md:basis-1/3 lg:basis-1/4 xl:basis-1/5'
className='py-4 pl-3 md:basis-1/3 lg:basis-1/4'
>
<div className='p-1'>
<Card className='border-[2px] border-brand-violet'>
<CardContent className='aspect-card flex items-center justify-center p-1'>
<CarouselItem>
<Image
src={card.image}
width={256}
height={256}
className='object-scale-down'
priority
alt={''}
/>
</CarouselItem>
</CardContent>
</Card>
<FeatureCardFront card={card} />
</div>
</CarouselItem>
))}

View File

@@ -1,39 +1,37 @@
'use client'
import cookies from 'js-cookie'
import {useLocale} from 'next-intl'
import {redirect} from 'next/navigation'
import {Link, usePathname, useRouter} from '@/i18n/routing'
import {useRouter} from '@/i18n/routing'
import {Link} from '@/i18n/routing'
import {Label} from '@/ui/label'
import {Switch} from '@/ui/switch'
export default function LocaleSwitcher() {
const router = useRouter()
const pathname = usePathname()
const locale = useLocale()
const initialState = locale !== 'uk'
//const [localeState, setLocaleState] = useState(initialState)
const handler = (state: boolean) => {
const newPath = `/${locale}${pathname}`
//window.history.replaceState(null, '', newPath)
const link = document.getElementById('lang-switch')
if (link) {
link.innerText = `${state ? '/ru' : ''}${pathname}`
link.setAttribute('href', `${state ? '/ru' : ''}${pathname}`)
link.click()
}
const handleLocaleChange = (state: boolean) => {
const locale = cookies.get('NEXT_LOCALE') === 'ru' ? 'uk' : 'ru'
cookies.set('NEXT_LOCALE', locale, {
expires: 7,
path: '/',
sameSite: 'Lax'
})
router.replace(window.location.pathname.replace(/^\/ru/, ''), {locale})
}
// router.replace('/cabinet', {locale: checked ? 'ru' : 'uk'}
return (
<div className='flex items-center space-x-2'>
<Link id='lang-switch' href='/' locale='uk'>
LA
</Link>
<Label htmlFor='locale-switcher'>Укр</Label>
<Switch
className='bg-brand-violet-900'
id='locale-switcher'
defaultChecked={initialState}
onCheckedChange={handler}
onCheckedChange={handleLocaleChange}
/>
<Label htmlFor='locale-switcher'>Рус</Label>
</div>

View File

@@ -1,9 +1,8 @@
'use client'
import {Menu as MenuIcon, X} from 'lucide-react'
import Link from 'next/link'
import {useState} from 'react'
import {Link} from '@/i18n/routing'
import {data} from '@/lib/data'
import {Button} from '@/ui/button'
@@ -21,7 +20,7 @@ export default function NavbarMenu() {
<div className={`hidden ${bp}:block w-full`}>
<div className='flex items-center justify-between'>
{data.headerMenus.map(item => (
<Link href={item.href + item.slug} className='' key={item.name}>
<Link href='/about-us' className='' key={item.name}>
{item.name}
</Link>
))}

View File

@@ -12,18 +12,20 @@ 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'>
<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>
<span>
<svg
className='h-4 w-4 transform fill-current transition duration-300 ease-in-out group-hover:-rotate-180'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
>
<path d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z' />
</svg>
</span>
</Button>
<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>
<span>
<svg
className='h-4 w-4 transform fill-current transition duration-300 ease-in-out group-hover:-rotate-180'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
>
<path d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z' />
</svg>
</span>
</Button>
</Link>
<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
@@ -33,7 +35,7 @@ export default function AppCatalogRender(data: {items: Category[]}) {
<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]'
href={`/category/${item.id}-${item.locales[locale === 'uk' ? 0 : 1].slug}`}
href={`/category/${item.locales[locale === 'uk' ? 0 : 1].slug}`}
>
{item.locales[locale === 'uk' ? 0 : 1].title}
</Link>

View File

@@ -5,6 +5,7 @@ import {useTranslations} from 'next-intl'
import {dump} from '@/lib/utils'
import useCartStore, {CartItem} from '@/store/cart-store'
import {Button} from '@/ui/button'
export default function AddCartButton({product}: {product: CartItem}) {
const t = useTranslations('cart')
@@ -13,16 +14,20 @@ export default function AddCartButton({product}: {product: CartItem}) {
return (
<>
<button
className='flex flex-col items-center'
role='button'
{/*<Button
className='bw-cart-btn flex flex-col items-center border-0 shadow-none'
variant='outline'
title={t('basket')}
onClick={() => addItemToCart(product)}
>
<ShoppingCartIcon className='h-[21px] w-[21px]' />
</button>
<pre>{dump(cartItems)}</pre>
</Button>*/}
<ShoppingCartIcon
role='button'
className='bw-cart-btn h-[24px] w-[24px]'
onClick={() => addItemToCart(product)}
/>
</>
)
}

View File

@@ -0,0 +1,31 @@
'use client'
import {ShoppingCartIcon} from 'lucide-react'
import {useTranslations} from 'next-intl'
import useCartStore, {CartItem} from '@/store/cart-store'
import {Button} from '@/ui/button'
export default function CardBuyButton({
item,
isIcon
}: {
item: CartItem
isIcon?: boolean
}) {
const t = useTranslations('cart')
const addItemToCart = useCartStore(state => state.addItemToCart)
return (
<Button
className={`mr-2 ${isIcon ? '' : 'w-[80px]'} grow-0 shadow-white hover:shadow-md hover:shadow-brand-violet/50`}
onClick={() => addItemToCart(item)}
>
{isIcon ? (
<ShoppingCartIcon role='button' className='bw-cart-btn p-1' />
) : (
t('buy')
)}
</Button>
)
}

View File

@@ -0,0 +1,71 @@
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
}: {
card: CategoryPageSqlSchema
}) {
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>*/}
<Image
className='transition duration-300 hover:scale-110'
src={card?.image || ''}
width={card.imageWidth || 100}
height={card.imageHeight || 100}
/*className='object-scale-down'*/
priority
alt={''}
sizes='100vw'
style={{
objectFit: 'contain',
width: '100%',
height: 'calc(100% - 24px)'
}}
/>
<RateStars />
{/*</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=''>
<p className='ml-2 border-b border-b-brand-violet pl-2 pr-6 text-[16px]'>
{card.title}
</p>
<p className='pl-4 text-[16px] font-bold text-brand-violet'>
{parseFloat(card.price as string).toFixed(2)}
</p>
</div>
<CardBuyButton
item={{
id: card.productId,
quantity: 1,
title: card.title,
price: parseFloat(card.price as string).toFixed(2)
}}
isIcon={true}
/>
</div>
</Card>
)
}
// id: number
// quantity: number
// title: string
// price: string | any
// image?: string | null
// imageWidth?: number | null
// imageHeight?: number | null

View File

@@ -0,0 +1,65 @@
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>*/}
<Image
className='transition duration-300 hover:scale-110'
src={card.image}
width={card.imageWidth}
height={card.imageHeight}
/*className='object-scale-down'*/
priority
alt={''}
sizes='100vw'
style={{
objectFit: 'contain',
width: '100%',
height: 'calc(100% - 24px)'
}}
/>
<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=''>
<p className='ml-2 border-b border-b-brand-violet pl-2 pr-6'>
{card.title}
</p>
<p className='pl-4 text-[18px] font-bold text-brand-violet'>
{parseFloat(card.price).toFixed(2)}
</p>
</div>
<CardBuyButton
item={{
id: card.productId,
quantity: 1,
title: card.title,
price: parseFloat(card.price).toFixed(2),
image: card.image
}}
/>
</div>
</Card>
)
}
// id: number
// quantity: number
// title: string
// price: string | any
// image?: string | null
// imageWidth?: number | null
// imageHeight?: number | null

View File

@@ -0,0 +1,16 @@
import {Star} from 'lucide-react'
const startStroke = 1.5
const color = '#ffd139'
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>
)
}

120
components/ui/table.tsx Normal file
View File

@@ -0,0 +1,120 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -1,135 +0,0 @@
'use client'
import * as ToastPrimitives from '@radix-ui/react-toast'
import {type VariantProps, cva} from 'class-variance-authority'
import {X} from 'lucide-react'
import * as React from 'react'
import {cn} from '@/lib/utils'
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({className, ...props}, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{
variants: {
variant: {
default: 'border bg-background text-foreground',
destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground',
success:
'group text-emerald-950-foreground border-emerald-600 bg-emerald-200',
warning:
'group text-amber-950-foreground border-amber-700 bg-amber-200',
brand:
'text-brand-violet-950 border-brand-violet-200 bg-brand-violet-50'
}
},
defaultVariants: {
variant: 'brand'
}
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({className, variant, ...props}, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({variant}), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({className, ...props}, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors group-[.destructive]:border-muted/40 hover:bg-secondary group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground focus:outline-none focus:ring-1 focus:ring-ring group-[.destructive]:focus:ring-destructive disabled:pointer-events-none disabled:opacity-50',
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({className, ...props}, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity group-hover:opacity-100 group-[.destructive]:text-red-300 hover:text-foreground group-[.destructive]:hover:text-red-50 focus:opacity-100 focus:outline-none focus:ring-1 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className
)}
toast-close=''
{...props}
>
<X className='h-4 w-4' />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({className, ...props}, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold [&+div]:text-xs', className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({className, ...props}, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction
}

View File

@@ -1,35 +0,0 @@
"use client"
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}