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:
co-authored by
Claude Sonnet 5
parent
c2552af079
commit
66f92b634e
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user