Files
sanihome/src/app/checkout/page.tsx
T
amirhosein.ashourlooandClaude Sonnet 5 66f92b634e 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>
2026-09-06 08:06:30 +03:30

193 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}