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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user