190 lines
6.9 KiB
TypeScript
190 lines
6.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { EmptyState } from "@/components/ui/EmptyState";
|
|
import { postComment, type EntityComment, type TechnicalInfoField } from "@/lib/meshkee";
|
|
|
|
type Tab = "specs" | "review" | "comments";
|
|
|
|
const TABS: { id: Tab; label: string }[] = [
|
|
{ id: "specs", label: "مشخصات فنی" },
|
|
{ id: "review", label: "نقد و بررسی" },
|
|
{ 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 [comments, setComments] = useState(initialComments);
|
|
const [name, setName] = useState("");
|
|
const [email, setEmail] = useState("");
|
|
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 (!name.trim() || !text.trim()) return;
|
|
setBusy(true);
|
|
setError(null);
|
|
const res = await postComment({
|
|
entityType: "product",
|
|
entityId,
|
|
authorName: name.trim(),
|
|
authorEmail: email.trim() || undefined,
|
|
text: text.trim(),
|
|
});
|
|
setBusy(false);
|
|
if (!res.ok) {
|
|
setError(res.message ?? "ثبت نظر ناموفق بود.");
|
|
return;
|
|
}
|
|
setComments((prev) => [{ id: `local-${Date.now()}`, authorName: name.trim(), text: text.trim() }, ...prev]);
|
|
setText("");
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<form onSubmit={handleSubmit} className="mb-6 space-y-3">
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
<input
|
|
required
|
|
placeholder="نام شما"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none transition-colors focus:border-red"
|
|
/>
|
|
<input
|
|
type="email"
|
|
placeholder="ایمیل (اختیاری)"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none transition-colors focus:border-red"
|
|
/>
|
|
</div>
|
|
<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 || !name.trim() || !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>
|
|
|
|
{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 ?? "";
|
|
|
|
return (
|
|
<section className="mt-10 overflow-hidden rounded-2xl border border-border bg-surface">
|
|
<div className="flex flex-wrap items-center gap-x-7 border-b border-border px-5 pt-5 md:gap-x-10 md:px-7">
|
|
{TABS.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
type="button"
|
|
onClick={() => setActive(tab.id)}
|
|
className={`relative pb-4 text-sm transition-colors ${
|
|
active === tab.id ? "font-bold text-foreground" : "text-muted hover:text-red"
|
|
}`}
|
|
>
|
|
{tab.label}
|
|
{active === tab.id && (
|
|
<span className="absolute inset-x-0 -bottom-px h-0.5 bg-foreground" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="p-5 md:p-7">
|
|
<h2 className="mb-5 flex items-center gap-2 text-lg font-bold">
|
|
<span className="h-2 w-2 shrink-0 rounded-full bg-red" aria-hidden />
|
|
{activeLabel}
|
|
</h2>
|
|
|
|
{active === "specs" &&
|
|
(technicalInfo.length === 0 ? (
|
|
<EmptyState message="هنوز مشخصات فنیای برای این محصول ثبت نشده است." />
|
|
) : (
|
|
<div className="overflow-hidden rounded-xl border border-border">
|
|
<table className="w-full border-collapse text-right text-sm">
|
|
<tbody>
|
|
{technicalInfo.map((field, index) => (
|
|
<tr
|
|
key={`${field.label}-${index}`}
|
|
className="border-b border-border last:border-b-0 odd:bg-background/60"
|
|
>
|
|
<th
|
|
scope="row"
|
|
className="w-[30%] px-4 py-3.5 align-top font-bold text-foreground md:w-[22%] md:px-5"
|
|
>
|
|
{field.label}
|
|
</th>
|
|
<td className="px-4 py-3.5 leading-7 text-muted md:px-5">
|
|
{field.value}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
))}
|
|
|
|
{active === "review" &&
|
|
(descriptionHtml ? (
|
|
<div
|
|
className="prose prose-sm max-w-none"
|
|
dangerouslySetInnerHTML={{ __html: descriptionHtml }}
|
|
/>
|
|
) : (
|
|
<EmptyState message="هنوز نقد و بررسیای برای این محصول ثبت نشده است." />
|
|
))}
|
|
|
|
{active === "comments" && <CommentsPanel entityId={entityId} initialComments={initialComments} />}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|