Add customer auth, real cart/checkout, store-items pages, comments, contact form

- Real in-site login/register (password + SMS OTP) against Meshkee's
  /auth API, with self-healing access-token refresh (15 min lifetime) and
  an in-flight-refresh guard so AuthContext and CartContext don't race
  each other into a failed concurrent refresh.
- Real cart: header cart button/badge, a cart popup, and a working
  "add to cart" on both product cards (where a specific variant id is
  known) and the product/store-item detail buy-box (always resolves a
  purchasable variant, even for single-SKU products with no selectable
  variation).
- /checkout: address + cash/bank-transfer payment, posts to the real
  checkout API and creates an order; refreshes the cart afterward so the
  header badge doesn't go stale.
- New /store-items and /store-items/[id] pages (variant-level browsing,
  separate from the product-level /products catalog), cross-referencing
  the parent product for images/description/technical-info.
- Product gallery: click-to-open fullscreen lightbox alongside the
  existing hover-zoom.
- Comments tab wired to the real comments API, gated to logged-in users
  (a product decision — the API itself doesn't require auth) with a
  login prompt otherwise.
- Similar-products row on product/store-item detail pages.
- Contact page wired to the real contact-submissions API, with a toast
  and inline confirmation on success, and the form resetting.
- CONTEXT.md: a maintained architecture/conventions doc for this repo,
  now auto-loaded via CLAUDE.md, covering the Meshkee integration quirks
  and UI conventions established across this project.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
amirhosein.ashourloo
2026-09-06 08:06:30 +03:30
co-authored by Claude Sonnet 5
parent c2552af079
commit 66f92b634e
30 changed files with 2344 additions and 37 deletions
+26
View File
@@ -0,0 +1,26 @@
"use client";
import { useAuth } from "@/context/AuthContext";
import { useCart } from "@/context/CartContext";
export function CartButton() {
const { user } = useAuth();
const { itemCount, openPopup } = useCart();
const { openLoginModal } = useAuth();
return (
<button
type="button"
onClick={user ? openPopup : openLoginModal}
className="relative flex items-center gap-1.5 rounded-lg border border-border px-3 py-2 transition-colors hover:border-red hover:text-red"
>
<span aria-hidden>🛒</span>
<span className="hidden sm:inline">سبد خرید</span>
{itemCount > 0 && (
<span className="absolute -top-1.5 -left-1.5 flex h-5 min-w-5 items-center justify-center rounded-full bg-red px-1 text-[10px] font-bold text-white">
{itemCount}
</span>
)}
</button>
);
}
+132
View File
@@ -0,0 +1,132 @@
"use client";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { formatPrice } from "@/data/sample";
import { useCart } from "@/context/CartContext";
import { EmptyState } from "@/components/ui/EmptyState";
export function CartPopup() {
const { cart, popupOpen, closePopup, updateItem, removeItem } = useCart();
const [mounted, setMounted] = useState(false);
const [busyId, setBusyId] = useState<string | null>(null);
useEffect(() => setMounted(true), []);
useEffect(() => {
document.body.style.overflow = popupOpen ? "hidden" : "";
return () => {
document.body.style.overflow = "";
};
}, [popupOpen]);
if (!mounted) return null;
const items = cart?.items ?? [];
const changeQty = async (itemId: string, quantity: number) => {
setBusyId(itemId);
if (quantity <= 0) await removeItem(itemId);
else await updateItem(itemId, quantity);
setBusyId(null);
};
return createPortal(
<div aria-hidden={!popupOpen}>
<div
onClick={closePopup}
className={`fixed inset-0 z-[100] bg-black/50 backdrop-blur-sm transition-opacity duration-300 ${
popupOpen ? "opacity-100" : "pointer-events-none opacity-0"
}`}
/>
<div
className={`fixed top-0 right-0 z-[110] flex h-dvh w-[90%] max-w-md flex-col bg-surface shadow-2xl transition-transform duration-300 ease-out ${
popupOpen ? "translate-x-0" : "translate-x-full"
}`}
>
<div className="flex items-center justify-between border-b border-border p-5">
<h2 className="text-lg font-bold">سبد خرید</h2>
<button
type="button"
onClick={closePopup}
aria-label="بستن"
className="flex h-9 w-9 items-center justify-center rounded-lg text-xl text-muted transition-colors hover:bg-red-light hover:text-red"
>
✕
</button>
</div>
<div className="flex-1 overflow-y-auto p-5">
{items.length === 0 ? (
<EmptyState message="سبد خرید شما خالی است." />
) : (
<ul className="space-y-4">
{items.map((item) => (
<li key={item.id} className="flex gap-3 border-b border-border pb-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={item.productImage ?? "/images/placeholder.svg"}
alt={item.productNameFa ?? item.productTitle}
className="h-16 w-16 shrink-0 rounded-lg border border-border object-contain p-1"
/>
<div className="min-w-0 flex-1">
<p className="line-clamp-2 text-sm font-medium">
{item.productNameFa ?? item.productTitle}
</p>
{item.label && <p className="mt-0.5 text-xs text-muted">{item.label}</p>}
<div className="mt-2 flex items-center justify-between">
<div className="flex items-center rounded-lg border border-border">
<button
type="button"
disabled={busyId === item.id}
onClick={() => changeQty(item.id, item.quantity + 1)}
className="px-2 py-1 text-sm disabled:opacity-40"
aria-label="افزایش تعداد"
>
+
</button>
<span className="min-w-6 text-center text-xs font-medium">{item.quantity}</span>
<button
type="button"
disabled={busyId === item.id}
onClick={() => changeQty(item.id, item.quantity - 1)}
className="px-2 py-1 text-sm disabled:opacity-40"
aria-label="کاهش تعداد"
>
−
</button>
</div>
<span className="text-sm font-bold text-red-dark">
{formatPrice(item.lineTotal)}
<span className="mr-0.5 text-[11px] font-normal text-muted">تومان</span>
</span>
</div>
</div>
</li>
))}
</ul>
)}
</div>
{items.length > 0 && cart && (
<div className="border-t border-border p-5">
<div className="mb-4 flex items-center justify-between text-sm">
<span className="text-muted">جمع کل</span>
<span className="text-base font-bold text-red-dark">
{formatPrice(cart.subtotal)}
<span className="mr-0.5 text-xs font-normal text-muted">تومان</span>
</span>
</div>
<a
href="/checkout"
className="block w-full rounded-xl bg-red py-3 text-center text-sm font-medium text-white transition-colors hover:bg-red-dark"
>
ادامه فرآیند خرید
</a>
</div>
)}
</div>
</div>,
document.body,
);
}