cart mechanism complete
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -125,3 +125,4 @@ dist
|
||||
.pnp.js
|
||||
.env*.local
|
||||
/messages/*.d.json.ts
|
||||
/public/uploads/
|
||||
|
||||
2
.idea/codeStyles/Project.xml
generated
2
.idea/codeStyles/Project.xml
generated
@@ -54,7 +54,7 @@
|
||||
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
|
||||
<option name="ARRAY_INITIALIZER_WRAP" value="5" />
|
||||
<option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" />
|
||||
<option name="SOFT_MARGINS" value="92" />
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
|
||||
4
.idea/watcherTasks.xml
generated
Normal file
4
.idea/watcherTasks.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions" suppressed-tasks="SCSS" />
|
||||
</project>
|
||||
31
app/[locale]/(root)/(shop)/cart/page.tsx
Normal file
31
app/[locale]/(root)/(shop)/cart/page.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import {useTranslations} from 'next-intl'
|
||||
|
||||
import CartItems from '@/components/pages/cart/items'
|
||||
|
||||
export default function Cart() {
|
||||
const t = useTranslations('cart')
|
||||
|
||||
// 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='mx-auto my-8 max-w-[640px] text-brand-violet'>
|
||||
<h1 className='text-3xl font-bold'>{t('basket')}</h1>
|
||||
<div className='bsdg-brand-violet-200 my-4 grid grid-cols-3 gap-4 border-t-2 border-brand-violet py-4'>
|
||||
<div className='bg-brand-violet-100'>Назва</div>
|
||||
<div className='bg-brand-violet-100'>Кількість</div>
|
||||
<div className='bg-brand-violet-100'>Вартість</div>
|
||||
</div>
|
||||
<CartItems />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import {Metadata} from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Checkout'
|
||||
}
|
||||
|
||||
export default function CheckoutPage() {
|
||||
//throw new Error('NOT IMPLEMENTED')
|
||||
|
||||
//const session = await auth()
|
||||
return <div>CheckoutPage</div>
|
||||
}
|
||||
16
app/[locale]/(root)/(shop)/product/[[...slug]]/page.tsx
Normal file
16
app/[locale]/(root)/(shop)/product/[[...slug]]/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import {notFound} from 'next/navigation'
|
||||
|
||||
import ProductPageIndex from '@/components/pages/product'
|
||||
|
||||
export default async function Products({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{slug?: string}>
|
||||
}) {
|
||||
const {slug} = await params
|
||||
const [uri] = slug || []
|
||||
const id = (uri || '').match(/^(\d+)-./)
|
||||
if (!id) notFound()
|
||||
|
||||
return <ProductPageIndex id={id[1]} />
|
||||
}
|
||||
@@ -43,22 +43,6 @@ export default async function HomePage() {
|
||||
</div>
|
||||
{/*<pre>{JSON.stringify(session)}</pre>*/}
|
||||
|
||||
<section className='relative mx-auto mt-8 h-[640px] w-[840px] bg-brand-violet-200'>
|
||||
<Image
|
||||
src={'/uploads/products/IMG_6572.jpg'}
|
||||
//fill
|
||||
//sizes='(min-width: 808px) 50vw, 100vw'
|
||||
width={1280}
|
||||
height={1280}
|
||||
alt=''
|
||||
title=''
|
||||
priority
|
||||
style={{
|
||||
objectFit: 'contain' // cover, contain, none
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className='mb-4 mt-[128px]'>
|
||||
<div className='container'>
|
||||
<FeatureCards />
|
||||
|
||||
@@ -214,3 +214,25 @@ body {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bw-product__text * {
|
||||
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !important;
|
||||
font-weight: 400 !important;
|
||||
font-size: 1rem !important;
|
||||
line-height: 1.45 !important;
|
||||
color: rgb(40, 26, 76) !important;
|
||||
|
||||
}
|
||||
.bw-product__text {
|
||||
h2 * {
|
||||
font-weight: 700 !important;
|
||||
font-size: 1.375rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bw-cart-item-counter {
|
||||
input {
|
||||
@apply text-xl leading-none border-0 text-brand-violet font-bold;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type {Metadata} from 'next'
|
||||
import {headers} from 'next/headers'
|
||||
import {ReactNode} from 'react'
|
||||
import {Toaster} from 'react-hot-toast'
|
||||
|
||||
import './globals.css'
|
||||
import {Toaster} from '@/components/ui/toaster'
|
||||
//import {Toaster} from '@/components/ui/toaster'
|
||||
import {routing} from '@/i18n/routing'
|
||||
import {APP_DESCRIPTION, APP_NAME, APP_SLOGAN} from '@/lib/constants'
|
||||
|
||||
@@ -25,7 +26,8 @@ export default async function RootLayout({
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<body className='min-h-screen antialiased'>
|
||||
{children}
|
||||
<Toaster />
|
||||
{/*<Toaster />*/}
|
||||
<Toaster position='top-right' reverseOrder={false} />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ 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,
|
||||
|
||||
4
components/pages/cart/cart.module.scss
Normal file
4
components/pages/cart/cart.module.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
input.bw-cart-item-counter{
|
||||
font-size: 36px;
|
||||
background: chocolate;
|
||||
}
|
||||
74
components/pages/cart/items.tsx
Normal file
74
components/pages/cart/items.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
// import styles from '@/components/pages/cart/cart.module.scss'
|
||||
import Link from 'next/link'
|
||||
|
||||
import useCartStore from '@/store/cart-store'
|
||||
import {Button} from '@/ui/button'
|
||||
import {Input} from '@/ui/input'
|
||||
|
||||
export default function CartItems() {
|
||||
const {cartItems} = useCartStore()
|
||||
|
||||
const {increaseQuantity, decreaseQuantity, removeItemFromCart} =
|
||||
useCartStore()
|
||||
const onIncreaseQuantity = (productId: number) => {
|
||||
increaseQuantity(productId)
|
||||
}
|
||||
|
||||
const onDecreaseQuantity = (productId: number) => {
|
||||
decreaseQuantity(productId)
|
||||
}
|
||||
|
||||
const onRemoveItem = (productId: number) => {
|
||||
removeItemFromCart(productId)
|
||||
}
|
||||
|
||||
if (cartItems && cartItems.length < 1) {
|
||||
return (
|
||||
<div className='flex h-72 flex-col items-center justify-center'>
|
||||
<h2 className='mb-5 mt-10 text-3xl font-bold'>Cart is Empty</h2>
|
||||
<Link
|
||||
href={'/products'}
|
||||
className='rounded-md bg-orange-500 px-6 py-2 text-white'
|
||||
>
|
||||
Shop
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className='text-3 flex-auto text-right font-bold'>
|
||||
{(item.price * item.quantity).toFixed(2)} грн
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
28
components/pages/product/add-cart-button.tsx
Normal file
28
components/pages/product/add-cart-button.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
57
components/pages/product/index.tsx
Normal file
57
components/pages/product/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import {getLocale} 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 {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]))
|
||||
|
||||
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>
|
||||
<Tabs defaultValue='article' className=''>
|
||||
<TabsList className='grid w-full grid-cols-2'>
|
||||
<TabsTrigger value='article'>Опис</TabsTrigger>
|
||||
<TabsTrigger value='instuction'>Інструкція</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='article'>
|
||||
<article
|
||||
className='bw-product__text'
|
||||
dangerouslySetInnerHTML={{__html: locale.content as string}}
|
||||
></article>
|
||||
</TabsContent>
|
||||
<TabsContent value='instuction'>
|
||||
<div
|
||||
className='bw-product__text'
|
||||
dangerouslySetInnerHTML={{__html: locale.instruction as string}}
|
||||
></div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -23,7 +23,6 @@ export default async function CabinetButton() {
|
||||
) : (
|
||||
<>
|
||||
<CircleUserRound className='h-[21px] w-[21px]' />
|
||||
GA4_Ecommerce_View_Item_List_Trigger
|
||||
</>
|
||||
)}
|
||||
{/*<span className='text-sm'>Кабінет</span>*/}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {Heart, ShoppingCartIcon, UserIcon} from 'lucide-react'
|
||||
import {Heart} from 'lucide-react'
|
||||
import {useTranslations} from 'next-intl'
|
||||
|
||||
import CabinetButton from '@/components/shared/header/cabinet-button'
|
||||
import HeaderShoppingCartIcon from '@/components/shared/header/shopping-cart-icon'
|
||||
import {Link} from '@/i18n/routing'
|
||||
|
||||
export default function HeaderControls() {
|
||||
@@ -18,17 +19,7 @@ export default function HeaderControls() {
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={'/checkout' as never}
|
||||
className='header-button'
|
||||
aria-label='Кошик'
|
||||
>
|
||||
<button className='flex flex-col items-center' role='button'>
|
||||
<ShoppingCartIcon className='h-[21px] w-[21px]' />
|
||||
|
||||
<span className='font1-bold text-sm'>{t('basket')}</span>
|
||||
</button>
|
||||
</Link>
|
||||
<HeaderShoppingCartIcon />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
25
components/shared/header/shopping-cart-icon.tsx
Normal file
25
components/shared/header/shopping-cart-icon.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import {ShoppingCartIcon} from 'lucide-react'
|
||||
import {useTranslations} from 'next-intl'
|
||||
|
||||
import {Link} from '@/i18n/routing'
|
||||
import useCartStore from '@/store/cart-store'
|
||||
|
||||
export default function HeaderShoppingCartIcon() {
|
||||
const t = useTranslations('cart')
|
||||
const {cartItems} = useCartStore()
|
||||
const cartCount = cartItems.length
|
||||
|
||||
return (
|
||||
<Link href={'/cart' as never} className='header-button' aria-label='Кошик'>
|
||||
<button className='flex flex-col items-center' role='button'>
|
||||
<ShoppingCartIcon className='h-[21px] w-[21px]' />
|
||||
|
||||
<span className='font1-bold text-sm'>
|
||||
{t('basket')} [{cartCount}]
|
||||
</span>
|
||||
</button>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import NavbarMenu from '@/components/shared/navbar/navbar-menu'
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<nav className='text-min flex w-full items-center justify-between text-sm font-medium leading-none'>
|
||||
<nav className='text-min flex w-full items-center justify-between text-sm font-medium leading-none text-brand-violet'>
|
||||
<NavbarMenu />
|
||||
</nav>
|
||||
)
|
||||
|
||||
@@ -27,18 +27,18 @@ export default function NavbarMenu() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-center ${bp}:hidden`}>
|
||||
{/*<div className={`flex items-center ${bp}:hidden`}>
|
||||
<Button variant='ghost' onClick={ToggleNavbar}>
|
||||
{menuOpened ? <X /> : <MenuIcon />}
|
||||
</Button>
|
||||
</div>
|
||||
{menuOpened && (
|
||||
</div>*/}
|
||||
{/*{menuOpened && (
|
||||
<div className={`${bp}:hidden`}>
|
||||
<div className='space-y-1 px-2 pb-3 pt-2'>
|
||||
<Link href={'#'}>Hidden Menu</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}*/}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
'use server'
|
||||
|
||||
import {Lang, Product, ProductLocale} from '@prisma/client'
|
||||
import internal from 'node:stream'
|
||||
import {Lang, Product, ProductLocale, ProductToStore} from '@prisma/client'
|
||||
|
||||
import {db, dbQueryLog} from '@/lib/db/prisma/client'
|
||||
|
||||
export interface ProductProps extends Product {
|
||||
locales: ProductLocale[]
|
||||
toStore: ProductToStore[]
|
||||
}
|
||||
|
||||
export const getProductBySlug = async (data: {
|
||||
slug: string
|
||||
lang: string
|
||||
|
||||
85
package-lock.json
generated
85
package-lock.json
generated
@@ -41,15 +41,16 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-hot-toast": "^2.5.1",
|
||||
"redis": "^4.7.0",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"server-only": "^0.0.1",
|
||||
"slugify": "^1.6.6",
|
||||
"swr": "^2.3.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "^3.24.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@@ -3790,7 +3791,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
@@ -3936,15 +3936,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||
@@ -5267,6 +5258,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/goober": {
|
||||
"version": "2.1.16",
|
||||
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
|
||||
"integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"csstype": "^3.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -7300,6 +7300,23 @@
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.1.tgz",
|
||||
"integrity": "sha512-54Gq1ZD1JbmAb4psp9bvFHjS7lje+8ubboUmvKZkCsQBLH6AOpZ9JemfRvIdHcfb9AZXRaFLrb3qUobGYDJhFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.3",
|
||||
"goober": "^2.1.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-dom": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -8167,19 +8184,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/swr": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.0.tgz",
|
||||
"integrity": "sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
|
||||
@@ -8610,6 +8614,8 @@
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
|
||||
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
@@ -8890,6 +8896,35 @@
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
|
||||
"integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,15 +64,16 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-hot-toast": "^2.5.1",
|
||||
"redis": "^4.7.0",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"server-only": "^0.0.1",
|
||||
"slugify": "^1.6.6",
|
||||
"swr": "^2.3.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "^3.24.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 381 KiB |
96
store/cart-store.ts
Normal file
96
store/cart-store.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import toast from 'react-hot-toast'
|
||||
import {create} from 'zustand'
|
||||
import {persist} from 'zustand/middleware'
|
||||
|
||||
export interface CartItem {
|
||||
id: number
|
||||
quantity: number
|
||||
title: string
|
||||
price: string | any
|
||||
image?: string | null
|
||||
}
|
||||
|
||||
interface CartState {
|
||||
cartItems: CartItem[]
|
||||
addItemToCart: (item: CartItem) => void
|
||||
increaseQuantity: (productId: number) => void
|
||||
decreaseQuantity: (productId: number) => void
|
||||
removeItemFromCart: (productId: number) => void
|
||||
}
|
||||
|
||||
const useCartStore = create(
|
||||
persist<CartState>(
|
||||
(set, get) => ({
|
||||
cartItems: [],
|
||||
|
||||
addItemToCart: item => {
|
||||
const itemExists = get().cartItems.find(
|
||||
cartItem => cartItem.id === item.id
|
||||
)
|
||||
|
||||
if (itemExists) {
|
||||
if (typeof itemExists.quantity === 'number') {
|
||||
itemExists.quantity++
|
||||
}
|
||||
|
||||
set({cartItems: [...get().cartItems]})
|
||||
} else {
|
||||
set({cartItems: [...get().cartItems, {...item, quantity: 1}]})
|
||||
}
|
||||
},
|
||||
|
||||
increaseQuantity: productId => {
|
||||
const itemExists = get().cartItems.find(
|
||||
cartItem => cartItem.id === productId
|
||||
)
|
||||
|
||||
if (itemExists) {
|
||||
if (typeof itemExists.quantity === 'number') {
|
||||
itemExists.quantity++
|
||||
}
|
||||
|
||||
set({cartItems: [...get().cartItems]})
|
||||
}
|
||||
},
|
||||
decreaseQuantity: productId => {
|
||||
const itemExists = get().cartItems.find(
|
||||
cartItem => cartItem.id === productId
|
||||
)
|
||||
|
||||
if (itemExists) {
|
||||
if (typeof itemExists.quantity === 'number') {
|
||||
if (itemExists.quantity === 1) {
|
||||
const updatedCartItems = get().cartItems.filter(
|
||||
item => item.id !== productId
|
||||
)
|
||||
set({cartItems: updatedCartItems})
|
||||
} else {
|
||||
itemExists.quantity--
|
||||
set({cartItems: [...get().cartItems]})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeItemFromCart: productId => {
|
||||
const itemExists = get().cartItems.find(
|
||||
cartItem => cartItem.id === productId
|
||||
)
|
||||
|
||||
if (itemExists) {
|
||||
if (typeof itemExists.quantity === 'number') {
|
||||
const updatedCartItems = get().cartItems.filter(
|
||||
item => item.id !== productId
|
||||
)
|
||||
set({cartItems: updatedCartItems})
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'cart-items'
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
export default useCartStore
|
||||
Reference in New Issue
Block a user