Initial commit: Dcorya storefront on Meshkee Website API

Next.js/TypeScript/Tailwind rebuild of dcorya.com in Persian/RTL, wired to
the live Meshkee Website API for products, blog, comments, cart, and contact.
This commit is contained in:
Dcorya Store
2026-09-05 15:40:20 +03:30
commit c126a6914a
105 changed files with 10536 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
import Image from "next/image";
import Link from "next/link";
const stats = [
{ value: "۹۹٪", label: "کیفیت محصول" },
{ value: "۹۰٪", label: "تنوع محصول" },
{ value: "۹۰٪", label: "رضایت مشتری" },
];
export default function AboutDcorya() {
return (
<section className="mx-auto max-w-6xl px-4 py-16 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
<div className="relative order-2 aspect-4/3 overflow-hidden rounded-2xl lg:order-1">
<Image
src="/images/product-2.jpg"
alt="محصولات چوبی دکوریا"
fill
sizes="(max-width: 1024px) 100vw, 50vw"
className="object-cover"
/>
</div>
<div className="order-1 lg:order-2">
<p className="text-sm font-semibold tracking-widest text-primary" style={{ fontFamily: "var(--font-montserrat)" }}>
ABOUT DCORYA
</p>
<h2 className="mt-2 text-2xl font-bold sm:text-3xl">فروشگاه آنلاین دکوریا</h2>
<p className="mt-2 text-neutral-500">زیباترین دکوراتیو چوبی، کریستال و برنز</p>
<p className="mt-6 leading-8 text-neutral-600">
فروشگاه اینترنتی دکوریا یک فروشگاه آنلاین تخصصی در زمینه محصولات چوبی از جمله آینه
کنسول، بوفه، ویترین و... (معرق و نقاشی و برنز سبک اروپا) و کریستال با برندهای معتبر
دنیا (چک، لهستان، آلمان و...) است. لذت خرید اینترنتی را با دکوریا تجربه کنید!
</p>
<Link
href="/about"
className="mt-6 inline-block rounded-lg bg-primary px-7 py-3 text-sm font-bold text-white transition-colors hover:bg-primary-dark"
>
بیشتر بدانید
</Link>
<div className="mt-10 grid grid-cols-3 gap-4 border-t border-black/5 pt-8">
{stats.map((stat) => (
<div key={stat.label} className="text-center">
<p className="text-2xl font-bold text-primary sm:text-3xl" style={{ fontFamily: "var(--font-montserrat)" }}>
{stat.value}
</p>
<p className="mt-1 text-xs font-medium text-neutral-500 sm:text-sm">{stat.label}</p>
</div>
))}
</div>
</div>
</div>
</section>
);
}
+52
View File
@@ -0,0 +1,52 @@
"use client";
import { useState } from "react";
export default function Accordion({
items,
}: {
items: { question: string; answer: string }[];
}) {
const [openIndex, setOpenIndex] = useState<number | null>(0);
return (
<div className="space-y-3">
{items.map((item, i) => {
const isOpen = openIndex === i;
return (
<div
key={item.question}
className="overflow-hidden rounded-xl border border-black/5 bg-white shadow-sm"
>
<button
onClick={() => setOpenIndex(isOpen ? null : i)}
className="flex w-full items-center justify-between gap-4 px-5 py-4 text-right"
aria-expanded={isOpen}
>
<span className="font-bold text-neutral-800">{item.question}</span>
<span
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary transition-transform duration-300 ${
isOpen ? "rotate-45" : ""
}`}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-4 w-4">
<path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</span>
</button>
<div
className="grid transition-all duration-300 ease-in-out"
style={{ gridTemplateRows: isOpen ? "1fr" : "0fr" }}
>
<div className="overflow-hidden">
<p className="border-t border-black/5 px-5 py-4 leading-8 text-neutral-600">
{item.answer}
</p>
</div>
</div>
</div>
);
})}
</div>
);
}
+95
View File
@@ -0,0 +1,95 @@
"use client";
import { useState } from "react";
import { useAuth } from "@/lib/auth-context";
import { useCart } from "@/lib/cart-context";
import { useToast } from "@/lib/toast-context";
import type { StoreItemVariant } from "@/lib/meshkee";
function formatToman(value?: number | null) {
if (value === null || value === undefined) return null;
return `${value.toLocaleString("fa-IR")} تومان`;
}
export default function AddToCartBox({ variant }: { variant: StoreItemVariant | null }) {
const { user, loginUrl } = useAuth();
const { addItem } = useCart();
const { showToast } = useToast();
const [quantity, setQuantity] = useState(1);
const [isSubmitting, setIsSubmitting] = useState(false);
const inStock = variant ? variant.unlimited || (variant.stockQuantity ?? 0) > 0 : false;
if (!variant) {
return (
<div className="rounded-xl border border-black/5 bg-neutral-50 p-5 text-center text-sm text-neutral-500">
این محصول در حال حاضر برای خرید آنلاین موجود نیست. برای اطلاع از قیمت و موجودی با ما تماس
بگیرید.
</div>
);
}
if (!inStock) {
return (
<div className="rounded-xl border border-black/5 bg-neutral-50 p-5 text-center">
<p className="mb-1 text-lg font-extrabold text-neutral-400" style={{ fontFamily: "var(--font-montserrat)" }}>
{formatToman(variant.price)}
</p>
<p className="text-sm font-medium text-neutral-500">ناموجود</p>
</div>
);
}
const handleAdd = async () => {
if (!user) {
window.location.href = loginUrl;
return;
}
setIsSubmitting(true);
try {
await addItem(variant.id, quantity);
showToast("محصول به سبد خرید اضافه شد");
} catch {
showToast("افزودن به سبد خرید با خطا مواجه شد", "error");
} finally {
setIsSubmitting(false);
}
};
return (
<div className="rounded-xl border border-black/5 bg-neutral-50 p-5">
<p
className="mb-4 text-2xl font-extrabold text-primary"
style={{ fontFamily: "var(--font-montserrat)" }}
>
{formatToman(variant.price)}
</p>
<div className="flex items-center gap-4">
<div className="flex items-center gap-3 rounded-lg border border-black/10 bg-white px-2 py-1.5">
<button
onClick={() => setQuantity((q) => Math.max(1, q - 1))}
className="flex h-7 w-7 items-center justify-center rounded-md text-neutral-600 hover:text-primary"
>
−
</button>
<span className="w-6 text-center text-sm font-medium">{quantity}</span>
<button
onClick={() => setQuantity((q) => q + 1)}
className="flex h-7 w-7 items-center justify-center rounded-md text-neutral-600 hover:text-primary"
>
+
</button>
</div>
<button
onClick={handleAdd}
disabled={isSubmitting}
className="flex-1 rounded-lg bg-primary py-3 text-sm font-bold text-white transition-colors hover:bg-primary-dark disabled:opacity-60"
>
{isSubmitting ? "در حال افزودن..." : "افزودن به سبد خرید"}
</button>
</div>
</div>
);
}
+43
View File
@@ -0,0 +1,43 @@
import Image from "next/image";
import Link from "next/link";
export default function BannerRow() {
return (
<section className="mx-auto grid max-w-6xl grid-cols-1 gap-6 px-4 py-4 sm:px-6 md:grid-cols-2 lg:px-8">
<Link
href="#"
className="group relative block overflow-hidden rounded-2xl bg-white shadow-sm"
>
<Image
src="/images/banner-1.png"
alt="فروش ویژه دکوریا"
width={900}
height={484}
className="h-56 w-full object-cover transition-transform duration-500 group-hover:scale-105 sm:h-72"
/>
</Link>
<Link
href="#"
className="group relative block overflow-hidden rounded-2xl shadow-sm"
>
<Image
src="/images/console-cat-4.jpg"
alt="کلکسیون میز کنسول"
width={900}
height={484}
className="h-56 w-full object-cover transition-transform duration-500 group-hover:scale-105 sm:h-72"
/>
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/40 text-center text-white">
<p className="text-xs tracking-[0.3em]" style={{ fontFamily: "var(--font-montserrat)" }}>
NEW COLLECTION
</p>
<h3 className="text-2xl font-bold">کلکسیون میز کنسول</h3>
<span className="mt-2 rounded-full border border-white/70 px-5 py-2 text-sm font-medium transition-colors group-hover:bg-white group-hover:text-neutral-900">
مشاهده محصولات
</span>
</div>
</Link>
</section>
);
}
+43
View File
@@ -0,0 +1,43 @@
"use client";
import { useRef, type ReactNode } from "react";
export default function Carousel({ children }: { children: ReactNode }) {
const trackRef = useRef<HTMLDivElement>(null);
const scrollByAmount = (dir: 1 | -1) => {
const el = trackRef.current;
if (!el) return;
el.scrollBy({ left: dir * -el.clientWidth * 0.85, behavior: "smooth" });
};
return (
<div className="relative">
<div
ref={trackRef}
className="flex snap-x snap-mandatory gap-5 overflow-x-auto scroll-smooth pb-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
>
{children}
</div>
<button
aria-label="قبلی"
onClick={() => scrollByAmount(-1)}
className="absolute -right-4 top-1/2 hidden h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-black/10 bg-white text-neutral-600 shadow-md transition-colors hover:text-primary sm:flex"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-5 w-5">
<path d="M9 6l6 6-6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<button
aria-label="بعدی"
onClick={() => scrollByAmount(1)}
className="absolute -left-4 top-1/2 hidden h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-black/10 bg-white text-neutral-600 shadow-md transition-colors hover:text-primary sm:flex"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-5 w-5">
<path d="M15 6l-6 6 6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
</div>
);
}
+102
View File
@@ -0,0 +1,102 @@
"use client";
import { useRef, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { useCart } from "@/lib/cart-context";
import { useAuth } from "@/lib/auth-context";
import { useClickOutside } from "@/lib/use-click-outside";
function formatToman(value?: number) {
if (!value && value !== 0) return null;
return `${value.toLocaleString("fa-IR")} تومان`;
}
export default function CartPopover() {
const { user, loginUrl } = useAuth();
const { cart, itemCount } = useCart();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useClickOutside(ref, () => setOpen(false));
const items = cart?.items ?? [];
return (
<div className="relative" ref={ref}>
<button
aria-label="سبد خرید"
onClick={() => setOpen((v) => !v)}
className="relative text-neutral-600 transition-colors hover:text-primary"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-5 w-5">
<path
d="M3 4h2l1.4 11.2A2 2 0 0 0 8.4 17H18a2 2 0 0 0 2-1.6L21.5 8H6"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle cx="9.5" cy="20.5" r="1.3" fill="currentColor" />
<circle cx="17.5" cy="20.5" r="1.3" fill="currentColor" />
</svg>
{itemCount > 0 && (
<span className="absolute -top-2 -left-2 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-white">
{itemCount}
</span>
)}
</button>
{open && (
<div className="absolute left-0 top-full z-50 mt-3 w-80 rounded-2xl border border-black/5 bg-white p-4 text-right shadow-2xl">
{!user ? (
<div className="py-4 text-center">
<p className="mb-4 text-sm text-neutral-500">
برای مشاهده سبد خرید ابتدا وارد حساب کاربری خود شوید.
</p>
<a
href={loginUrl}
className="inline-block rounded-lg bg-primary px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-primary-dark"
>
ورود به حساب کاربری
</a>
</div>
) : items.length === 0 ? (
<p className="py-6 text-center text-sm text-neutral-500">سبد خرید شما خالی است.</p>
) : (
<>
<div className="max-h-72 space-y-3 overflow-y-auto">
{items.map((item) => (
<div key={item.id} className="flex items-center gap-3">
<div className="relative h-14 w-14 shrink-0 overflow-hidden rounded-lg bg-neutral-100">
{item.image && (
<Image src={item.image} alt={item.title ?? ""} fill className="object-cover" />
)}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-neutral-800">
{item.title ?? "کالا"}
</p>
<p className="text-xs text-neutral-500">تعداد: {item.quantity}</p>
</div>
{formatToman(item.unitPrice) && (
<p className="shrink-0 text-xs font-bold text-primary">
{formatToman(item.unitPrice)}
</p>
)}
</div>
))}
</div>
<Link
href="/cart"
onClick={() => setOpen(false)}
className="mt-4 block rounded-lg bg-primary py-2.5 text-center text-sm font-bold text-white transition-colors hover:bg-primary-dark"
>
مشاهده سبد خرید و ادامه
</Link>
</>
)}
</div>
)}
</div>
);
}
+104
View File
@@ -0,0 +1,104 @@
"use client";
import { useState } from "react";
import { useAuth } from "@/lib/auth-context";
import { useToast } from "@/lib/toast-context";
import { meshkee, type Comment } from "@/lib/meshkee";
function formatDate(iso: string) {
try {
return new Intl.DateTimeFormat("fa-IR", { year: "numeric", month: "long", day: "numeric" }).format(
new Date(iso)
);
} catch {
return "";
}
}
export default function CommentsSection({
entityType,
entityId,
initialComments,
}: {
entityType: "product" | "blog" | "portfolio" | "video";
entityId: string;
initialComments: Comment[];
}) {
const { user, loginUrl } = useAuth();
const { showToast } = useToast();
const [comments, setComments] = useState(initialComments);
const [text, setText] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!user || !text.trim()) return;
setIsSubmitting(true);
try {
const { comment } = await meshkee.postComment({
entityType,
entityId,
authorName: `${user.firstName} ${user.lastName}`.trim(),
authorEmail: user.email ?? undefined,
text: text.trim(),
});
setComments((prev) => [comment, ...prev]);
setText("");
showToast("نظر شما با موفقیت ثبت شد و پس از تایید نمایش داده می‌شود");
} catch {
showToast("ثبت نظر با خطا مواجه شد", "error");
} finally {
setIsSubmitting(false);
}
};
return (
<div>
{user ? (
<form onSubmit={handleSubmit} className="mb-8">
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
required
rows={4}
placeholder="نظر خود را درباره این محصول بنویسید..."
className="w-full rounded-lg border border-neutral-200 px-4 py-3 text-sm outline-none transition-colors focus:border-primary"
/>
<button
type="submit"
disabled={isSubmitting}
className="mt-3 rounded-lg bg-primary px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-primary-dark disabled:opacity-60"
>
{isSubmitting ? "در حال ارسال..." : "ثبت نظر"}
</button>
</form>
) : (
<div className="mb-8 rounded-xl border border-black/5 bg-neutral-50 p-5 text-center">
<p className="mb-3 text-sm text-neutral-500">برای ثبت نظر ابتدا وارد حساب کاربری خود شوید.</p>
<a
href={loginUrl}
className="inline-block rounded-lg bg-primary px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-primary-dark"
>
ورود به حساب کاربری
</a>
</div>
)}
{comments.length === 0 ? (
<p className="text-sm text-neutral-500">هنوز نظری برای این محصول ثبت نشده است.</p>
) : (
<div className="space-y-5">
{comments.map((comment) => (
<div key={comment.id} className="rounded-xl border border-black/5 bg-white p-4">
<div className="mb-1.5 flex items-center justify-between">
<span className="text-sm font-bold text-neutral-800">{comment.authorName}</span>
<span className="text-xs text-neutral-400">{formatDate(comment.createdAt)}</span>
</div>
<p className="text-sm leading-7 text-neutral-600">{comment.text}</p>
</div>
))}
</div>
)}
</div>
);
}
+49
View File
@@ -0,0 +1,49 @@
import Image from "next/image";
import Link from "next/link";
const items = [
{ title: "سه تیکه", icon: "/images/console-cat-1.png" },
{ title: "انواع میز پذیرایی", icon: "/images/console-cat-2.png" },
{ title: "قلمکاری روی مس", icon: "/images/console-cat-3.png" },
{ title: "انواع شمعــــدان", icon: "/images/console-cat-4.jpg" },
{ title: "میز کنسول", icon: "/images/console-cat-5.png" },
{ title: "میز تی وی", icon: "/images/console-cat-6.png" },
];
export default function ConsoleCategories() {
return (
<section className="bg-neutral-50 py-16">
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<div className="mb-12 text-center">
<h2 className="text-2xl font-bold text-neutral-800 sm:text-3xl">
زیباترین دکوراتیو چوبی، کریستال و برنز
</h2>
<p className="mt-2 text-neutral-500">
به ما اعتماد کنید! زیرا ما بهترین کیفیت را ارائه می دهیم
</p>
</div>
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-6">
{items.map((item) => (
<Link
key={item.title}
href="#"
className="group flex flex-col items-center gap-3 rounded-2xl border border-black/5 bg-white p-5 text-center shadow-sm transition-shadow hover:shadow-lg"
>
<div className="relative h-20 w-20 overflow-hidden rounded-full bg-neutral-50">
<Image
src={item.icon}
alt={item.title}
fill
sizes="80px"
className="object-cover transition-transform group-hover:scale-110"
/>
</div>
<h3 className="text-sm font-medium text-neutral-700">{item.title}</h3>
</Link>
))}
</div>
</div>
</section>
);
}
+79
View File
@@ -0,0 +1,79 @@
"use client";
import { useRef, useState } from "react";
import { meshkee } from "@/lib/meshkee";
import { useToast } from "@/lib/toast-context";
export default function ContactForm() {
const { showToast } = useToast();
const formRef = useRef<HTMLFormElement>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const data = new FormData(form);
const name = String(data.get("name") || "").trim();
const email = String(data.get("email") || "").trim();
const cellNumber = String(data.get("cellNumber") || "").trim();
const text = String(data.get("text") || "").trim();
if (!name || !text) return;
setIsSubmitting(true);
try {
await meshkee.postContactSubmission({
title: `پیام از سایت - ${name}`,
name,
email: email || undefined,
cellNumber: cellNumber || undefined,
text,
});
showToast("پیام شما با موفقیت ارسال شد");
formRef.current?.reset();
} catch {
showToast("ارسال پیام با خطا مواجه شد. لطفا دوباره تلاش کنید.", "error");
} finally {
setIsSubmitting(false);
}
};
return (
<form ref={formRef} onSubmit={handleSubmit} className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<input
required
name="name"
type="text"
placeholder="نام و نام خانوادگی"
className="rounded-lg border border-neutral-200 px-4 py-3 text-sm outline-none transition-colors focus:border-primary"
/>
<input
name="email"
type="email"
placeholder="ایمیل"
className="rounded-lg border border-neutral-200 px-4 py-3 text-sm outline-none transition-colors focus:border-primary"
/>
<input
name="cellNumber"
type="tel"
placeholder="شماره تماس"
className="rounded-lg border border-neutral-200 px-4 py-3 text-sm outline-none transition-colors focus:border-primary sm:col-span-2"
/>
<textarea
required
name="text"
rows={5}
placeholder="پیام شما"
className="rounded-lg border border-neutral-200 px-4 py-3 text-sm outline-none transition-colors focus:border-primary sm:col-span-2"
/>
<button
type="submit"
disabled={isSubmitting}
className="justify-self-start rounded-lg bg-primary px-8 py-3 text-sm font-bold text-white transition-colors hover:bg-primary-dark disabled:opacity-60 sm:col-span-2"
>
{isSubmitting ? "در حال ارسال..." : "ارسال پیام"}
</button>
</form>
);
}
+86
View File
@@ -0,0 +1,86 @@
import Link from "next/link";
const cards = [
{
title: "به ما ایمیل بزنید",
value: "dcorya.shopp@gmail.com",
href: "mailto:dcorya.shopp@gmail.com",
ltr: true,
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-7 w-7">
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" strokeWidth="1.6" />
<path d="m4 6.5 8 6 8-6" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
),
},
{
title: "همیشه پاسخگوی شما هستیم",
value: "025-32400725",
href: "tel:02532400725",
ltr: true,
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-7 w-7">
<path
d="M6.5 3.5c1 0 2.7 2.6 2.7 3.7 0 1.1-1.6 1.8-1.9 2.6-.4 1 1.2 3 2.4 4.2 1.2 1.2 3.2 2.8 4.2 2.4.8-.3 1.5-1.9 2.6-1.9 1.1 0 3.7 1.7 3.7 2.7 0 1-2.7 3.3-3.7 3.3-1.7 0-5.5-1.7-8.8-5-3.3-3.3-5-7.1-5-8.8 0-1 2.3-3.7 3.3-3.7z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</svg>
),
},
{
title: "فروشگاه دکوریا",
value: "قم، خیابان ۴۵ متری صدوقی، نبش حافظ",
href: "#",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-7 w-7">
<path
d="M12 21s7-6.5 7-11.5a7 7 0 1 0-14 0C5 14.5 12 21 12 21z"
stroke="currentColor"
strokeWidth="1.6"
strokeLinejoin="round"
/>
<circle cx="12" cy="9.5" r="2.3" stroke="currentColor" strokeWidth="1.6" />
</svg>
),
},
];
export default function ContactTeaser() {
return (
<section className="bg-neutral-50 py-16">
<div className="mx-auto max-w-6xl px-4 text-center sm:px-6 lg:px-8">
<p className="text-sm font-semibold tracking-widest text-primary" style={{ fontFamily: "var(--font-montserrat)" }}>
GET IN TOUCH
</p>
<h2 className="mt-2 text-2xl font-bold sm:text-3xl">هر طور که راحتید با ما تماس بگیرید</h2>
<div className="mt-12 overflow-hidden rounded-2xl border border-black/5 bg-white shadow-lg sm:grid sm:grid-cols-3 sm:divide-x sm:divide-x-reverse sm:divide-black/5">
{cards.map((card) => (
<Link
key={card.title}
href={card.href}
className="group flex flex-col items-center gap-3 border-b border-black/5 px-6 py-10 transition-colors last:border-b-0 hover:bg-primary/5 sm:border-b-0"
>
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-primary text-white shadow-md shadow-primary/20 transition-transform group-hover:scale-105">
{card.icon}
</div>
<h3 className="font-bold text-neutral-800">{card.title}</h3>
<p dir={card.ltr ? "ltr" : undefined} className="text-sm leading-7 text-neutral-500">
{card.value}
</p>
</Link>
))}
</div>
<Link
href="/contact"
className="mt-12 inline-block rounded-lg bg-primary px-8 py-3 text-sm font-bold text-white transition-colors hover:bg-primary-dark"
>
تماس با ما
</Link>
</div>
</section>
);
}
+32
View File
@@ -0,0 +1,32 @@
import Carousel from "./Carousel";
import ProductCard from "./ProductCard";
const products = [
{ image: "/images/decor-1.jpg", title: "ساعت و شمعدان دو پسر برنز طلایی" },
{ image: "/images/decor-2.jpg", title: "ساعت و شمعدان زن و مرد برنز طلایی آنتیک" },
{ image: "/images/decor-3.jpg", title: "ساعت و شمعدان زن و مرد برنز طلایی" },
{ image: "/images/decor-4.jpg", title: "ساعت و شمعدان ریسه ای برنز آنتیک" },
{ image: "/images/decor-5.jpg", title: "ست ساعت و شمعدان پرنسس ۰۱۱" },
{ image: "/images/decor-6.jpg", title: "ست شمعدان و ساعت سنگ و برنز ۰۱۰" },
];
export default function DecorativeProducts() {
return (
<section className="bg-neutral-50 py-16">
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<div className="mb-10 text-center">
<p className="text-sm font-semibold tracking-widest text-primary" style={{ fontFamily: "var(--font-montserrat)" }}>
HANDCRAFTED
</p>
<h2 className="mt-2 text-2xl font-bold sm:text-3xl">صنایع دستی و تزئینی دکوریا</h2>
</div>
<Carousel>
{products.map((p) => (
<ProductCard key={p.title} {...p} />
))}
</Carousel>
</div>
</section>
);
}
+61
View File
@@ -0,0 +1,61 @@
import Image from "next/image";
import Link from "next/link";
export default function Footer() {
return (
<footer className="mt-auto border-t border-black/5 bg-neutral-50">
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-10 px-4 py-14 sm:px-6 lg:grid-cols-3 lg:px-8">
<div>
<Image src="/images/logo.png" alt="دکوریا" width={110} height={36} className="h-9 w-auto" />
<p className="mt-4 text-sm leading-7 text-neutral-500">
زیباترین دکوراتیو چوبی، کریستال، برنز و صنایع دستی نفیس. لذت خرید اینترنتی را اینبار با
دکوریا تجربه کنید!
</p>
</div>
<div>
<h3 className="mb-4 font-bold text-neutral-800">خدمات مشتریان</h3>
<ul className="space-y-2 text-sm text-neutral-500">
<li>
<Link href="#" className="transition-colors hover:text-primary">
رضایت مشتریان
</Link>
</li>
<li>
<Link href="/about" className="transition-colors hover:text-primary">
درباره دکوریا
</Link>
</li>
<li>
<Link href="/contact" className="transition-colors hover:text-primary">
تماس با دکوریا
</Link>
</li>
</ul>
</div>
<div>
<h3 className="mb-4 font-bold text-neutral-800">اطلاعات تماس</h3>
<p className="mb-1 text-sm text-neutral-500">همیشه پاسخگوی شما هستیم ....</p>
<p dir="ltr" className="text-right text-sm text-neutral-600">
025-32400725
<br />
09121514070
<br />
09129368019
<br />
09124592669
</p>
<p className="mt-3 text-sm text-neutral-600">قم، خیابان 45 متری صدوقی، نبش حافظ</p>
<p dir="ltr" className="mt-1 text-right text-sm text-neutral-600">
dcorya.shopp@gmail.com
</p>
</div>
</div>
<div className="border-t border-black/5 py-5 text-center text-xs text-neutral-400">
© {new Date().getFullYear()} فروشگاه اینترنتی دکوریا. تمامی حقوق محفوظ است.
</div>
</footer>
);
}
+59
View File
@@ -0,0 +1,59 @@
import Image from "next/image";
import Link from "next/link";
import CartPopover from "./CartPopover";
import ProfileMenu from "./ProfileMenu";
const navLinks = [
{ href: "/", label: "صفحه اصلی" },
{ href: "/products", label: "محصولات" },
{ href: "/blog", label: "بلاگ" },
{ href: "/satisfaction", label: "مشتریان راضی" },
{ href: "/purchaseguide", label: "راهنمای خرید" },
];
export default function Header() {
return (
<header className="sticky top-0 z-50 h-16 border-b border-black/10 bg-white shadow-[0_2px_10px_rgba(0,0,0,0.06)]">
<div className="mx-auto flex h-full max-w-7xl items-center justify-between gap-6 px-4 sm:px-6 lg:px-8">
<Link href="/" className="shrink-0">
<Image
src="/images/logo.png"
alt="دکوریا"
width={110}
height={36}
priority
className="h-9 w-auto"
/>
</Link>
<nav className="hidden items-center gap-7 lg:flex">
{navLinks.map((link) => (
<Link
key={link.label}
href={link.href}
className="text-sm font-medium text-neutral-700 transition-colors hover:text-primary"
>
{link.label}
</Link>
))}
</nav>
<div className="flex items-center gap-5">
<button
aria-label="جستجو"
className="text-neutral-600 transition-colors hover:text-primary"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-5 w-5">
<circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="1.8" />
<path d="m20 20-3.5-3.5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
</button>
<CartPopover />
<ProfileMenu />
</div>
</div>
</header>
);
}
+109
View File
@@ -0,0 +1,109 @@
"use client";
import Image from "next/image";
import { useCallback, useEffect, useRef, useState } from "react";
const slides = [
{ src: "/images/slide-1.jpg", alt: "کریستال و ظروف دکوراتیو دکوریا" },
{ src: "/images/slide-2.jpg", alt: "فروش اقساطی بدون سود محصولات دکوریا" },
{ src: "/images/slide-3.jpg", alt: "قلمکاری مس، هنر اصیل ایرانی دکوریا" },
{ src: "/images/slide-4.jpg", alt: "میز کنسول چوبی معرق‌کاری دکوریا" },
];
export default function Hero() {
const [active, setActive] = useState(0);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const goTo = useCallback((index: number) => {
setActive((index + slides.length) % slides.length);
}, []);
useEffect(() => {
timerRef.current = setInterval(() => {
setActive((prev) => (prev + 1) % slides.length);
}, 5500);
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, []);
return (
<section className="relative h-[calc(100dvh-4rem)] min-h-[420px] w-full overflow-hidden">
{slides.map((slide, i) => (
<div
key={slide.src}
className={`absolute inset-0 transition-opacity duration-1000 ease-in-out ${
i === active ? "opacity-100" : "opacity-0"
}`}
>
<Image
src={slide.src}
alt={slide.alt}
fill
priority={i === 0}
sizes="100vw"
className="object-cover"
/>
</div>
))}
<div className="absolute inset-x-0 bottom-0 z-10 flex justify-start px-4 pb-20 sm:px-10 sm:pb-24">
<div
key={active}
className="max-w-xs rounded-2xl bg-white/95 px-6 py-6 text-right shadow-2xl backdrop-blur-sm sm:max-w-md sm:px-10 sm:py-8"
>
<span
className="animate-fade-in-up mb-2 block text-2xl tracking-widest text-primary sm:text-3xl"
style={{ fontFamily: "var(--font-montserrat)", animationDelay: "0.1s" }}
>
Dcorya
</span>
<h1
className="animate-fade-in-up text-lg font-bold leading-relaxed text-neutral-800 sm:text-2xl"
style={{ animationDelay: "0.25s" }}
>
ارسال به سراسر ایران و کشورهای همسایه
</h1>
<p
className="animate-fade-in-up mt-2 text-xs tracking-wide text-neutral-500 sm:text-sm"
style={{ fontFamily: "var(--font-montserrat)", animationDelay: "0.4s" }}
>
Shipping Across Iran and Neighboring Countries
</p>
</div>
</div>
<button
aria-label="اسلاید قبلی"
onClick={() => goTo(active - 1)}
className="absolute top-1/2 right-4 z-10 flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full bg-white/20 text-white backdrop-blur-sm transition-colors hover:bg-white/40 sm:right-8"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-5 w-5">
<path d="M9 6l6 6-6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<button
aria-label="اسلاید بعدی"
onClick={() => goTo(active + 1)}
className="absolute top-1/2 left-4 z-10 flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full bg-white/20 text-white backdrop-blur-sm transition-colors hover:bg-white/40 sm:left-8"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-5 w-5">
<path d="M15 6l-6 6 6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<div className="absolute bottom-6 left-6 z-10 flex gap-2 sm:left-10">
{slides.map((slide, i) => (
<button
key={slide.src}
aria-label={`اسلاید ${i + 1}`}
onClick={() => goTo(i)}
className={`h-2.5 rounded-full transition-all ${
i === active ? "w-7 bg-white" : "w-2.5 bg-white/50"
}`}
/>
))}
</div>
</section>
);
}
+42
View File
@@ -0,0 +1,42 @@
import Link from "next/link";
import Carousel from "./Carousel";
import ProductCard from "./ProductCard";
import RemoteProductCard from "./RemoteProductCard";
import { meshkee } from "@/lib/meshkee";
const fallbackProducts = [
{ image: "/images/product-1.jpg", title: "ساعت ایستاده CL001", price: "29,700,000 تومان" },
{ image: "/images/product-2.jpg", title: "میز کنسول سه درب نیم گرد نقاشی برنزخور B084", price: "49,300,000 تومان" },
{ image: "/images/product-3.jpg", title: "میز کنسول دودرب نقاشی برنزخور B003", price: "47,500,000 تومان" },
{ image: "/images/product-4.jpg", title: "میز کنسول سه درب نقاشی برنزخور B096", price: "49,500,000 تومان" },
{ image: "/images/product-5.jpg", title: "آینه کنسول تک درب نقاشی برنزخور B020", price: "43,500,000 تومان" },
{ image: "/images/product-6.jpg", title: "میز کنسول تک درب نقاشی برنزخور B075", price: "35,500,000 تومان" },
{ image: "/images/product-7.jpg", title: "میز کنسول ویترین نقاشی برنزخور B083", price: "46,900,000 تومان" },
{ image: "/images/product-8.jpg", title: "میز کنسول خمره ای وسط کشو نقاشی برنزخور B087", price: "49,700,000 تومان" },
];
export default async function LatestProducts() {
const { items } = await meshkee.getProducts({ pageSize: 8 }).catch(() => ({ items: [] }));
return (
<section className="mx-auto max-w-6xl px-4 py-16 sm:px-6 lg:px-8">
<div className="mb-10 flex flex-col items-center gap-3 text-center sm:flex-row sm:items-end sm:justify-between sm:text-right">
<div>
<p className="text-sm font-semibold tracking-widest text-primary" style={{ fontFamily: "var(--font-montserrat)" }}>
NEW ARRIVALS
</p>
<h2 className="mt-2 text-2xl font-bold sm:text-3xl">جدیدترین پیشنهادهای دکوریا</h2>
</div>
<Link href="/products" className="text-sm font-bold text-primary hover:underline">
مشاهده همه محصولات ←
</Link>
</div>
<Carousel>
{items.length > 0
? items.map((p) => <RemoteProductCard key={p.id} product={p} />)
: fallbackProducts.map((p) => <ProductCard key={p.title} {...p} />)}
</Carousel>
</section>
);
}
+27
View File
@@ -0,0 +1,27 @@
export default function PageHeader({
title,
subtitle,
backgroundImage = "/images/page-header-bg.jpg",
}: {
title: string;
subtitle: string;
backgroundImage?: string;
}) {
return (
<section
className="relative flex h-56 items-center justify-center overflow-hidden bg-[#faf8f5] bg-cover bg-center sm:h-64"
style={{ backgroundImage: `url('${backgroundImage}')` }}
>
<div className="absolute inset-0 bg-white/40" />
<div className="relative z-10 text-center">
<h1 className="text-3xl font-bold text-neutral-700 sm:text-4xl">{title}</h1>
<p
className="mt-2 text-xs tracking-[0.3em] text-neutral-400"
style={{ fontFamily: "var(--font-montserrat)" }}
>
{subtitle}
</p>
</div>
</section>
);
}
+42
View File
@@ -0,0 +1,42 @@
import Image from "next/image";
import Link from "next/link";
export default function ProductCard({
image,
title,
price,
}: {
image: string;
title: string;
price?: string;
}) {
return (
<Link
href="#"
className="group w-[80vw] shrink-0 snap-start overflow-hidden rounded-2xl border border-black/5 bg-white shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-xl sm:w-[300px]"
>
<div className="relative aspect-4/3 overflow-hidden bg-neutral-50">
<Image
src={image}
alt={title}
fill
sizes="(max-width: 640px) 80vw, 300px"
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
</div>
<div className="p-5 text-center">
<h3 className="line-clamp-2 min-h-[2.8em] text-base font-semibold text-neutral-800">
{title}
</h3>
{price && (
<p
className="mt-3 text-lg font-extrabold text-primary"
style={{ fontFamily: "var(--font-montserrat)" }}
>
{price}
</p>
)}
</div>
</Link>
);
}
+54
View File
@@ -0,0 +1,54 @@
import Image from "next/image";
import Link from "next/link";
const categories = [
{ title: "هنرهای دستی", subtitle: "ARTS AND CRAFTS", icon: "/images/cat-crafts.png", href: "#" },
{ title: "دکوری و تزئینی", subtitle: "DECORATION", icon: "/images/cat-decoration.png", href: "#" },
{ title: "آشپز خانه", subtitle: "KITCHEN", icon: "/images/cat-kitchen.png", href: "#" },
{ title: "محصولات چوبی", subtitle: "WOOD PRODUCTS", icon: "/images/cat-wood.png", href: "#" },
];
export default function ProductCategories() {
return (
<section
className="relative bg-cover bg-center py-16"
style={{ backgroundImage: "url('/images/category-bg.jpg')" }}
>
<div className="absolute inset-0 bg-white/60" />
<div className="relative mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<div className="mb-12 text-center">
<h2 className="text-2xl font-bold text-neutral-800 sm:text-3xl">دکوریا در یک نگاه</h2>
<p
className="mt-2 text-xs tracking-[0.3em] text-neutral-400"
style={{ fontFamily: "var(--font-montserrat)" }}
>
WWW.DCORYA.COM
</p>
</div>
<div className="grid grid-cols-2 gap-6 sm:grid-cols-4">
{categories.map((cat) => (
<Link
key={cat.title}
href={cat.href}
className="group flex flex-col items-center gap-4 rounded-2xl bg-white/80 p-6 text-center shadow-sm backdrop-blur-sm transition-shadow hover:shadow-lg"
>
<div className="flex h-20 w-20 items-center justify-center">
<Image src={cat.icon} alt={cat.title} width={64} height={64} className="h-16 w-16 object-contain transition-transform group-hover:scale-110" />
</div>
<div>
<h3 className="font-bold text-neutral-800">{cat.title}</h3>
<p
className="mt-1 text-[10px] tracking-[0.2em] text-neutral-400"
style={{ fontFamily: "var(--font-montserrat)" }}
>
{cat.subtitle}
</p>
</div>
</Link>
))}
</div>
</div>
</section>
);
}
+123
View File
@@ -0,0 +1,123 @@
"use client";
import Image from "next/image";
import { useState, useRef, type MouseEvent } from "react";
export default function ProductGallery({
images,
title,
}: {
images: string[];
title: string;
}) {
const [active, setActive] = useState(0);
const [lightboxOpen, setLightboxOpen] = useState(false);
const [zoomPos, setZoomPos] = useState({ x: 50, y: 50 });
const [isHovering, setIsHovering] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const gallery = images.length > 0 ? images : ["/images/logo.png"];
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
const el = containerRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
setZoomPos({ x, y });
};
const goTo = (index: number) => setActive((index + gallery.length) % gallery.length);
return (
<div>
<div
ref={containerRef}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
onMouseMove={handleMouseMove}
onClick={() => setLightboxOpen(true)}
className="relative aspect-square cursor-zoom-in overflow-hidden rounded-2xl border border-black/5 bg-neutral-50"
>
<Image
src={gallery[active]}
alt={title}
fill
sizes="(max-width: 1024px) 100vw, 50vw"
priority
className="object-cover transition-transform duration-200 ease-out"
style={
isHovering
? { transform: "scale(1.8)", transformOrigin: `${zoomPos.x}% ${zoomPos.y}%` }
: undefined
}
/>
</div>
{gallery.length > 1 && (
<div className="mt-4 flex gap-3 overflow-x-auto pb-1">
{gallery.map((src, i) => (
<button
key={src}
onClick={() => goTo(i)}
className={`relative h-20 w-20 shrink-0 overflow-hidden rounded-xl border-2 transition-colors ${
i === active ? "border-primary" : "border-transparent"
}`}
>
<Image src={src} alt={`${title} ${i + 1}`} fill sizes="80px" className="object-cover" />
</button>
))}
</div>
)}
{lightboxOpen && (
<div
className="fixed inset-0 z-100 flex items-center justify-center bg-black/90 p-4"
onClick={() => setLightboxOpen(false)}
>
<button
aria-label="بستن"
className="absolute left-6 top-6 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20"
onClick={() => setLightboxOpen(false)}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-5 w-5">
<path d="M6 6l12 12M18 6 6 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</button>
<button
aria-label="قبلی"
onClick={(e) => {
e.stopPropagation();
goTo(active - 1);
}}
className="absolute top-1/2 right-4 flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 sm:right-8"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-5 w-5">
<path d="M9 6l6 6-6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<button
aria-label="بعدی"
onClick={(e) => {
e.stopPropagation();
goTo(active + 1);
}}
className="absolute top-1/2 left-4 flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 sm:left-8"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-5 w-5">
<path d="M15 6l-6 6 6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<div
className="relative h-[80vh] w-full max-w-3xl"
onClick={(e) => e.stopPropagation()}
>
<Image src={gallery[active]} alt={title} fill sizes="100vw" className="object-contain" />
</div>
</div>
)}
</div>
);
}
+98
View File
@@ -0,0 +1,98 @@
"use client";
import { useState } from "react";
import CommentsSection from "./CommentsSection";
import type { Comment, TechnicalInfoResponse } from "@/lib/meshkee";
type Tab = "description" | "specs" | "comments";
export default function ProductTabs({
descriptionHtml,
technicalInfo,
productId,
initialComments,
commentCount,
}: {
descriptionHtml: string;
technicalInfo: TechnicalInfoResponse | null;
productId: string;
initialComments: Comment[];
commentCount: number;
}) {
const hasSpecs = !!technicalInfo?.form?.fields?.length && technicalInfo.values.length > 0;
const tabs: { key: Tab; label: string }[] = [
{ key: "description", label: "توضیحات" },
...(hasSpecs ? ([{ key: "specs", label: "مشخصات فنی" }] as const) : []),
{ key: "comments", label: `نظرات (${commentCount})` },
];
const [active, setActive] = useState<Tab>("description");
const specRows =
hasSpecs && technicalInfo?.form
? [...technicalInfo.form.fields]
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
.map((field) => {
const value = technicalInfo.values.find((v) => v.fieldId === field.id);
if (!value) return null;
let display = value.textValue ?? "";
if (value.optionId && field.options) {
display = field.options.find((o) => o.id === value.optionId)?.label ?? display;
}
if (value.optionIds && field.options) {
display = value.optionIds
.map((id) => field.options?.find((o) => o.id === id)?.label)
.filter(Boolean)
.join("، ");
}
return { label: field.label, value: display };
})
.filter((row): row is { label: string; value: string } => !!row && !!row.value)
: [];
return (
<div>
<div className="mb-6 flex gap-2 border-b border-black/10">
{tabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActive(tab.key)}
className={`relative px-4 py-3 text-sm font-bold transition-colors ${
active === tab.key ? "text-primary" : "text-neutral-500 hover:text-neutral-700"
}`}
>
{tab.label}
{active === tab.key && (
<span className="absolute inset-x-0 -bottom-px h-0.5 rounded-full bg-primary" />
)}
</button>
))}
</div>
{active === "description" && (
<div
className="prose prose-neutral max-w-none text-neutral-600 [&_p]:leading-8"
dangerouslySetInnerHTML={{ __html: descriptionHtml || "<p>توضیحاتی برای این محصول ثبت نشده است.</p>" }}
/>
)}
{active === "specs" && hasSpecs && (
<table className="w-full overflow-hidden rounded-xl border border-black/5 text-sm">
<tbody>
{specRows.map((row, i) => (
<tr key={row.label} className={i % 2 === 0 ? "bg-neutral-50" : "bg-white"}>
<td className="w-1/3 px-4 py-3 font-medium text-neutral-500">{row.label}</td>
<td className="px-4 py-3 text-neutral-800">{row.value}</td>
</tr>
))}
</tbody>
</table>
)}
{active === "comments" && (
<CommentsSection entityType="product" entityId={productId} initialComments={initialComments} />
)}
</div>
);
}
+75
View File
@@ -0,0 +1,75 @@
"use client";
import { useRef, useState } from "react";
import { useAuth } from "@/lib/auth-context";
import { CUSTOMER_PORTAL_URL } from "@/lib/meshkee";
import { useClickOutside } from "@/lib/use-click-outside";
export default function ProfileMenu() {
const { user, isLoading, isAdmin, loginUrl, logout } = useAuth();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useClickOutside(ref, () => setOpen(false));
if (isLoading) {
return <span className="h-5 w-16 animate-pulse rounded bg-neutral-100" />;
}
if (!user) {
return (
<a
href={loginUrl}
className="hidden items-center gap-1.5 text-sm font-medium text-neutral-700 transition-colors hover:text-primary sm:flex"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-5 w-5">
<circle cx="12" cy="8" r="3.2" stroke="currentColor" strokeWidth="1.8" />
<path d="M5 20c1.2-3.5 4-5.5 7-5.5s5.8 2 7 5.5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
ورود
</a>
);
}
return (
<div className="relative" ref={ref}>
<button
onClick={() => setOpen((v) => !v)}
className="hidden items-center gap-1.5 text-sm font-medium text-neutral-700 transition-colors hover:text-primary sm:flex"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className="h-5 w-5">
<circle cx="12" cy="8" r="3.2" stroke="currentColor" strokeWidth="1.8" />
<path d="M5 20c1.2-3.5 4-5.5 7-5.5s5.8 2 7 5.5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
{user.firstName} عزیز
</button>
{open && (
<div className="absolute left-0 top-full z-50 mt-3 w-48 overflow-hidden rounded-xl border border-black/5 bg-white py-2 text-right shadow-2xl">
<a
href={`${CUSTOMER_PORTAL_URL}/profile`}
className="block px-4 py-2.5 text-sm text-neutral-700 transition-colors hover:bg-neutral-50 hover:text-primary"
>
پروفایل من
</a>
{isAdmin && (
<a
href={`${CUSTOMER_PORTAL_URL}/admin`}
className="block px-4 py-2.5 text-sm text-neutral-700 transition-colors hover:bg-neutral-50 hover:text-primary"
>
مدیریت وبسایت
</a>
)}
<button
onClick={() => {
logout();
setOpen(false);
}}
className="block w-full px-4 py-2.5 text-right text-sm text-neutral-700 transition-colors hover:bg-neutral-50 hover:text-primary"
>
خروج
</button>
</div>
)}
</div>
);
}
+53
View File
@@ -0,0 +1,53 @@
import Image from "next/image";
import Link from "next/link";
import type { Product } from "@/lib/meshkee";
import { slugify } from "@/lib/slug";
function formatToman(value?: number | null) {
if (value === null || value === undefined) return null;
return `${value.toLocaleString("fa-IR")} تومان`;
}
export default function RemoteProductCard({ product }: { product: Product }) {
const href = `/products/${product.id}/${slugify(product.nameFa || product.title)}`;
const price = formatToman(product.store?.minPrice);
return (
<Link
href={href}
className="group w-[80vw] shrink-0 snap-start overflow-hidden rounded-2xl border border-black/5 bg-white shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-xl sm:w-[300px]"
>
<div className="relative aspect-4/3 overflow-hidden bg-neutral-50">
{product.thumbnailUrl ? (
<Image
src={product.thumbnailUrl}
alt={product.nameFa || product.title}
fill
sizes="(max-width: 640px) 80vw, 300px"
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
) : (
<div className="flex h-full items-center justify-center text-neutral-300">بدون تصویر</div>
)}
{product.store && !product.store.inStock && (
<span className="absolute right-3 top-3 rounded-full bg-neutral-800/80 px-3 py-1 text-xs font-medium text-white">
ناموجود
</span>
)}
</div>
<div className="p-5 text-center">
<h3 className="line-clamp-2 min-h-[2.8em] text-base font-semibold text-neutral-800">
{product.nameFa || product.title}
</h3>
{price && (
<p
className="mt-3 text-lg font-extrabold text-primary"
style={{ fontFamily: "var(--font-montserrat)" }}
>
{price}
</p>
)}
</div>
</Link>
);
}