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,192 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Breadcrumb } from "@/components/ui/Breadcrumb";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useCart } from "@/context/CartContext";
|
||||
import { checkout, type Order } from "@/lib/cart";
|
||||
import { formatPrice } from "@/data/sample";
|
||||
|
||||
type PaymentType = "cash" | "transfer";
|
||||
|
||||
const inputClass =
|
||||
"w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none transition-colors focus:border-red";
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const { user, openLoginModal } = useAuth();
|
||||
const { cart, businessId, refresh } = useCart();
|
||||
const [province, setProvince] = useState("");
|
||||
const [city, setCity] = useState("");
|
||||
const [address, setAddress] = useState("");
|
||||
const [postalCode, setPostalCode] = useState("");
|
||||
const [paymentType, setPaymentType] = useState<PaymentType>("cash");
|
||||
const [transferRefNumber, setTransferRefNumber] = useState("");
|
||||
const [customerNotes, setCustomerNotes] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [order, setOrder] = useState<Order | null>(null);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
const res = await checkout(businessId, {
|
||||
shippingAddress: { province, city, address, postalCode: postalCode || undefined },
|
||||
customerNotes: customerNotes || undefined,
|
||||
payment:
|
||||
paymentType === "transfer"
|
||||
? { type: "transfer", transferRefNumber: transferRefNumber || undefined }
|
||||
: { type: "cash" },
|
||||
});
|
||||
setBusy(false);
|
||||
if (!res.ok) {
|
||||
setError(res.message);
|
||||
return;
|
||||
}
|
||||
setOrder(res.data);
|
||||
refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-4 py-10">
|
||||
<Breadcrumb items={[{ label: "خانه", href: "/" }, { label: "تسویه حساب" }]} />
|
||||
<h1 className="mb-6 text-2xl font-bold text-red-dark">تسویه حساب</h1>
|
||||
|
||||
{!user ? (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-border bg-surface p-5">
|
||||
<span className="text-sm text-muted">برای تسویه حساب باید وارد حساب کاربری خود شوید.</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openLoginModal}
|
||||
className="rounded-lg bg-red px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-red-dark"
|
||||
>
|
||||
ورود
|
||||
</button>
|
||||
</div>
|
||||
) : order ? (
|
||||
<div className="rounded-xl border border-green-200 bg-green-50 p-6 text-center">
|
||||
<p className="mb-2 text-lg font-bold text-green-800">سفارش شما با موفقیت ثبت شد ✓</p>
|
||||
<p className="mb-1 text-sm text-green-700">شماره سفارش: {order.orderNumber}</p>
|
||||
<p className="text-sm text-green-700">
|
||||
مبلغ کل: {formatPrice(order.total)} تومان
|
||||
</p>
|
||||
<a href="/" className="mt-4 inline-block text-sm font-medium text-red hover:text-red-dark">
|
||||
بازگشت به فروشگاه ←
|
||||
</a>
|
||||
</div>
|
||||
) : !cart || cart.items.length === 0 ? (
|
||||
<EmptyState message="سبد خرید شما خالی است." />
|
||||
) : (
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
<form onSubmit={handleSubmit} className="space-y-4 md:col-span-2">
|
||||
<div>
|
||||
<h2 className="mb-3 text-sm font-bold">آدرس ارسال</h2>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<input
|
||||
required
|
||||
placeholder="استان"
|
||||
value={province}
|
||||
onChange={(e) => setProvince(e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
<input
|
||||
required
|
||||
placeholder="شهر"
|
||||
value={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
required
|
||||
rows={2}
|
||||
placeholder="آدرس کامل"
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
className={`${inputClass} mt-3`}
|
||||
/>
|
||||
<input
|
||||
placeholder="کد پستی (اختیاری)"
|
||||
value={postalCode}
|
||||
onChange={(e) => setPostalCode(e.target.value)}
|
||||
className={`${inputClass} mt-3`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="mb-3 text-sm font-bold">روش پرداخت</h2>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 rounded-lg border border-border p-3 text-sm">
|
||||
<input
|
||||
type="radio"
|
||||
name="payment"
|
||||
checked={paymentType === "cash"}
|
||||
onChange={() => setPaymentType("cash")}
|
||||
/>
|
||||
پرداخت در محل (نقدی)
|
||||
</label>
|
||||
<label className="flex items-center gap-2 rounded-lg border border-border p-3 text-sm">
|
||||
<input
|
||||
type="radio"
|
||||
name="payment"
|
||||
checked={paymentType === "transfer"}
|
||||
onChange={() => setPaymentType("transfer")}
|
||||
/>
|
||||
کارت به کارت / واریز بانکی
|
||||
</label>
|
||||
{paymentType === "transfer" && (
|
||||
<input
|
||||
placeholder="شماره پیگیری واریز (اختیاری)"
|
||||
value={transferRefNumber}
|
||||
onChange={(e) => setTransferRefNumber(e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
rows={2}
|
||||
placeholder="توضیحات سفارش (اختیاری)"
|
||||
value={customerNotes}
|
||||
onChange={(e) => setCustomerNotes(e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
|
||||
{error && <p className="text-sm text-red-dark">{error}</p>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
className="w-full rounded-xl bg-red py-3 text-sm font-medium text-white transition-colors hover:bg-red-dark disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{busy ? "در حال ثبت سفارش..." : "ثبت نهایی سفارش"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="rounded-xl border border-border bg-surface p-5">
|
||||
<h2 className="mb-4 text-sm font-bold">خلاصه سفارش</h2>
|
||||
<ul className="mb-4 space-y-2 text-sm">
|
||||
{cart.items.map((item) => (
|
||||
<li key={item.id} className="flex items-center justify-between gap-2">
|
||||
<span className="line-clamp-1 text-muted">
|
||||
{item.productNameFa ?? item.productTitle} × {item.quantity}
|
||||
</span>
|
||||
<span className="shrink-0 font-medium">{formatPrice(item.lineTotal)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-center justify-between border-t border-border pt-3 text-sm font-bold">
|
||||
<span>جمع کل</span>
|
||||
<span className="text-red-dark">
|
||||
{formatPrice(cart.subtotal)}
|
||||
<span className="mr-0.5 text-xs font-normal text-muted">تومان</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user