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
@@ -2,7 +2,8 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import type { TechnicalInfoField } from "@/lib/meshkee";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { postComment, type EntityComment, type TechnicalInfoField } from "@/lib/meshkee";
|
||||
|
||||
type Tab = "specs" | "review" | "comments";
|
||||
|
||||
@@ -12,12 +13,105 @@ const TABS: { id: Tab; label: string }[] = [
|
||||
{ id: "comments", label: "نظرات کاربران" },
|
||||
];
|
||||
|
||||
function formatDate(iso?: string) {
|
||||
if (!iso) return "";
|
||||
return new Intl.DateTimeFormat("fa-IR", { dateStyle: "medium" }).format(new Date(iso));
|
||||
}
|
||||
|
||||
function CommentsPanel({ entityId, initialComments }: { entityId: string; initialComments: EntityComment[] }) {
|
||||
const { user, openLoginModal } = useAuth();
|
||||
const [comments, setComments] = useState(initialComments);
|
||||
const [text, setText] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!user || !text.trim()) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
const authorName = [user.firstName, user.lastName].filter(Boolean).join(" ") || "کاربر";
|
||||
const res = await postComment({
|
||||
entityType: "product",
|
||||
entityId,
|
||||
authorName,
|
||||
authorEmail: user.email,
|
||||
text: text.trim(),
|
||||
});
|
||||
setBusy(false);
|
||||
if (!res.ok) {
|
||||
setError(res.message ?? "ثبت نظر ناموفق بود.");
|
||||
return;
|
||||
}
|
||||
setComments((prev) => [{ id: `local-${Date.now()}`, authorName, text: text.trim() }, ...prev]);
|
||||
setText("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{user ? (
|
||||
<form onSubmit={handleSubmit} className="mb-6 space-y-3">
|
||||
<textarea
|
||||
required
|
||||
rows={3}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="نظر خود را درباره این محصول بنویسید..."
|
||||
className="w-full rounded-xl border border-border bg-background p-3 text-sm outline-none transition-colors focus:border-red"
|
||||
/>
|
||||
{error && <p className="text-xs text-red-dark">{error}</p>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={busy || !text.trim()}
|
||||
className="rounded-lg bg-red px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-red-dark disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{busy ? "در حال ثبت..." : "ثبت نظر"}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-3 rounded-xl bg-background p-4 text-sm">
|
||||
<span className="text-muted">برای ثبت نظر ابتدا وارد حساب کاربری خود شوید.</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openLoginModal}
|
||||
className="rounded-lg bg-red px-4 py-1.5 text-xs font-medium text-white transition-colors hover:bg-red-dark"
|
||||
>
|
||||
ورود
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{comments.length === 0 ? (
|
||||
<EmptyState message="هنوز نظری برای این محصول ثبت نشده است." />
|
||||
) : (
|
||||
<ul className="space-y-4">
|
||||
{comments.map((comment) => (
|
||||
<li key={comment.id} className="rounded-xl bg-background p-4">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className="text-sm font-bold">{comment.authorName}</span>
|
||||
{comment.createdAt && (
|
||||
<span className="text-xs text-muted">{formatDate(comment.createdAt)}</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted">{comment.text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProductTabs({
|
||||
technicalInfo,
|
||||
descriptionHtml,
|
||||
entityId,
|
||||
initialComments = [],
|
||||
}: {
|
||||
technicalInfo: TechnicalInfoField[];
|
||||
descriptionHtml?: string;
|
||||
entityId: string;
|
||||
initialComments?: EntityComment[];
|
||||
}) {
|
||||
const [active, setActive] = useState<Tab>("specs");
|
||||
const activeLabel = TABS.find((t) => t.id === active)?.label ?? "";
|
||||
@@ -72,9 +166,7 @@ export function ProductTabs({
|
||||
<EmptyState message="هنوز نقد و بررسیای برای این محصول ثبت نشده است." />
|
||||
))}
|
||||
|
||||
{active === "comments" && (
|
||||
<EmptyState message="هنوز نظری برای این محصول ثبت نشده است." />
|
||||
)}
|
||||
{active === "comments" && <CommentsPanel entityId={entityId} initialComments={initialComments} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user