feat: add cart and product pages, quantity controls, and first-visit development notice
Build & Deploy (develop) / build-and-deploy (push) Successful in 1m44s

This commit is contained in:
2026-09-15 07:14:37 +02:00
parent 4cd34106b0
commit 0390a35d01
23 changed files with 528 additions and 466 deletions
+68
View File
@@ -0,0 +1,68 @@
'use client';
import Link from 'next/link';
import {Button, Empty} from 'antd';
import {DeleteOutlined} from '@ant-design/icons';
import {useAppDispatch, useAppSelector} from '@/store/hooks';
import {removeFromCart, selectCartItems, selectCartTotal} from '@/store/cartSlice';
import {useLanguage} from '@/app/context/LanguageContext';
import {commerceCopy} from '@/data/commerce';
import QuantityControl from './QuantityControl';
export default function CartPage() {
const dispatch = useAppDispatch();
const items = useAppSelector(selectCartItems);
const total = useAppSelector(selectCartTotal);
const {t, locale} = useLanguage();
const copy = commerceCopy[locale];
const money = (value: number) => `${value.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US')} ${copy.currency}`;
return (
<main className="mx-auto max-w-7xl min-h-[65vh] px-5 md:px-8 pt-28 pb-20">
<Link href="/#showcase" className="text-brand-400 hover:underline">{copy.continueShopping}</Link>
<h1 className="text-3xl md:text-4xl font-extrabold text-primary mt-6">{t.cart.title}</h1>
<p className="text-secondary mt-3 mb-10">{copy.cartDescription}</p>
{items.length === 0 ? (
<div className="rounded-3xl border border-subtle bg-surface p-10 text-center">
<Empty description={t.cart.empty}/>
<Link href="/#showcase" className="inline-block mt-6 text-brand-400 hover:underline">{copy.continueShopping}</Link>
</div>
) : (
<div className="grid lg:grid-cols-[minmax(0,1fr),340px] gap-8 items-start">
<div className="space-y-4">
{items.map(item => {
const product = t.machines.find(machine => machine.id === item.id) ?? item;
return (
<article key={item.id} className="rounded-2xl border border-subtle bg-surface p-5 flex flex-wrap sm:flex-nowrap gap-5 items-center">
<Link href={`/products/${item.id}`} className="shrink-0">
<img src={product.image} alt={product.name} className="w-24 h-32 object-contain"/>
</Link>
<div className="flex-1 min-w-0">
<Link href={`/products/${item.id}`} className="font-bold text-lg text-primary hover:text-brand-400">{product.name}</Link>
<p className="text-secondary text-sm mt-2">{copy.unitPrice}: {money(item.priceNumeric)}</p>
<div className="flex flex-wrap gap-3 items-center mt-4">
<QuantityControl product={product}/>
<Button danger type="text" icon={<DeleteOutlined/>}
aria-label={`${copy.remove}: ${product.name}`}
onClick={() => dispatch(removeFromCart(item.id))}/>
</div>
<p className="mt-4 text-primary font-semibold">{copy.lineTotal}: {money(item.qty * item.priceNumeric)}</p>
</div>
</article>
);
})}
</div>
<aside className="lg:sticky lg:top-28 border border-subtle bg-elevated rounded-2xl p-6">
<h2 className="font-bold text-xl text-primary">{copy.summary}</h2>
<div className="border-t border-subtle mt-5 pt-5 flex flex-wrap justify-between gap-3">
<span className="text-secondary">{t.cart.total}</span>
<strong className="text-primary">{money(total)}</strong>
</div>
<p className="text-secondary text-sm leading-7 my-5">{copy.checkoutUnavailable}</p>
<Button type="primary" size="large" disabled block>{t.cart.checkout}</Button>
</aside>
</div>
)}
</main>
);
}