Wire Meshkee catalog: products, blog, mega menu, and product search.
Catalog-only (no cart); search targets store items and products pages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
cfa21cbee9
commit
8e14748b48
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { Placeholder } from "@/components/Placeholder";
|
||||
|
||||
export function ProductGallery({ images, alt }: { images: string[]; alt: string }) {
|
||||
const gallery = images.length ? images : ["/placeholders/product.svg"];
|
||||
const [active, setActive] = useState(0);
|
||||
const [lightbox, setLightbox] = useState(false);
|
||||
const [origin, setOrigin] = useState("50% 50%");
|
||||
const [zoomed, setZoomed] = useState(false);
|
||||
const current = gallery[active] ?? gallery[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="relative block w-full overflow-hidden rounded-2xl bg-paper"
|
||||
onClick={() => setLightbox(true)}
|
||||
onMouseEnter={() => setZoomed(true)}
|
||||
onMouseLeave={() => {
|
||||
setZoomed(false);
|
||||
setOrigin("50% 50%");
|
||||
}}
|
||||
onMouseMove={(event) => {
|
||||
const box = event.currentTarget.getBoundingClientRect();
|
||||
const x = ((event.clientX - box.left) / box.width) * 100;
|
||||
const y = ((event.clientY - box.top) / box.height) * 100;
|
||||
setOrigin(`${x}% ${y}%`);
|
||||
}}
|
||||
>
|
||||
<Placeholder
|
||||
src={current}
|
||||
fallbackSrc="/placeholders/product.svg"
|
||||
alt={alt}
|
||||
className="aspect-square w-full object-contain transition-transform duration-150"
|
||||
/>
|
||||
<span
|
||||
className="pointer-events-none absolute inset-0 bg-contain bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url(${current})`,
|
||||
transform: zoomed ? "scale(1.85)" : "scale(1)",
|
||||
transformOrigin: origin,
|
||||
opacity: zoomed ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
{gallery.length > 1 ? (
|
||||
<div className="mt-3 flex gap-2 overflow-x-auto">
|
||||
{gallery.map((image, index) => (
|
||||
<button
|
||||
key={`${image}-${index}`}
|
||||
type="button"
|
||||
onClick={() => setActive(index)}
|
||||
className={`size-16 shrink-0 overflow-hidden rounded-xl border ${index === active ? "border-brand" : "border-line"}`}
|
||||
>
|
||||
<Placeholder src={image} className="size-full object-contain" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{lightbox ? (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-ink/80 p-4" onClick={() => setLightbox(false)}>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-4 left-4 grid size-10 place-items-center rounded-full bg-white text-ink"
|
||||
aria-label="بستن"
|
||||
>
|
||||
<X className="size-5" />
|
||||
</button>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={current}
|
||||
alt={alt}
|
||||
className="max-h-[90vh] max-w-full rounded-2xl object-contain"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { customerPortalUrl } from "@/lib/config";
|
||||
import { formatFaDate } from "@/lib/format";
|
||||
import type { CommentItem } from "@/lib/comments";
|
||||
|
||||
export function ProductTabs({
|
||||
descriptionHtml,
|
||||
comments,
|
||||
showDescription = true,
|
||||
}: {
|
||||
descriptionHtml?: string | null;
|
||||
comments: CommentItem[];
|
||||
showDescription?: boolean;
|
||||
}) {
|
||||
const [tab, setTab] = useState<"description" | "comments">(showDescription ? "description" : "comments");
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setToken(
|
||||
window.localStorage.getItem("accessToken") ||
|
||||
window.localStorage.getItem("meshkeeToken") ||
|
||||
window.localStorage.getItem("token"),
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="mt-10 rounded-2xl border border-line bg-white p-5 sm:p-7">
|
||||
{showDescription ? (
|
||||
<div className="flex gap-2 border-b border-line pb-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTab("description")}
|
||||
className={`rounded-lg px-4 py-2 text-[13px] font-medium ${tab === "description" ? "bg-paper text-ink" : "text-muted"}`}
|
||||
>
|
||||
توضیحات
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTab("comments")}
|
||||
className={`rounded-lg px-4 py-2 text-[13px] font-medium ${tab === "comments" ? "bg-paper text-ink" : "text-muted"}`}
|
||||
>
|
||||
نظرات
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<h2 className="text-[16px] font-bold text-ink">نظرات</h2>
|
||||
)}
|
||||
|
||||
{tab === "description" ? (
|
||||
descriptionHtml ? (
|
||||
<div className="rich-text mt-5 text-[14px] leading-8 text-ink" dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
|
||||
) : (
|
||||
<p className="mt-5 text-[13px] text-muted">توضیحاتی برای این محصول ثبت نشده است.</p>
|
||||
)
|
||||
) : (
|
||||
<div className="mt-5 space-y-4">
|
||||
{comments.length === 0 ? (
|
||||
<p className="text-[13px] text-muted">نظری ثبت نشده است.</p>
|
||||
) : (
|
||||
comments.map((comment, index) => (
|
||||
<article key={comment.id ?? index} className="rounded-xl bg-paper p-4">
|
||||
<p className="text-[13px] font-bold text-ink">{comment.authorName || "کاربر"}</p>
|
||||
{comment.createdAt ? <p className="mt-1 text-[11px] text-muted">{formatFaDate(comment.createdAt)}</p> : null}
|
||||
<p className="mt-2 text-[13px] leading-7 text-ink">{comment.text}</p>
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
|
||||
{token ? (
|
||||
<p className="text-[13px] text-muted">برای مدیریت نظرات از حساب کاربری خود استفاده کنید.</p>
|
||||
) : (
|
||||
<div className="rounded-2xl bg-paper p-5">
|
||||
<p className="text-[14px] font-bold text-ink">برای ثبت نظر وارد شوید</p>
|
||||
<p className="mt-1 text-[13px] text-muted">نظرات فقط برای کاربران واردشده فعال است.</p>
|
||||
<a
|
||||
href={customerPortalUrl}
|
||||
className="mt-4 inline-flex h-11 items-center rounded-xl bg-brand px-5 text-[13px] font-bold text-white hover:bg-brand-dark"
|
||||
>
|
||||
ورود / ثبتنام
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user