Connect Meshkee APIs and add catalog, blog, and content pages.

Wire user-products and blogs to the Website API, add about/contact/categories routes, and polish flat UI filters and page banners.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-10 10:54:27 +03:30
co-authored by Cursor
parent e74e5ca45f
commit 8194b7292f
31 changed files with 2160 additions and 204 deletions
+5 -4
View File
@@ -1,4 +1,5 @@
import Image from "next/image";
import Link from "next/link";
import { site, stats } from "@/data/home";
export function AboutCta() {
@@ -41,12 +42,12 @@ export function AboutCta() {
</div>
<div className="mt-8 flex flex-wrap gap-3">
<a href="#contact" className="btn-primary">
<Link href="/contact" className="btn-primary">
تماس بگیرید
</a>
<a href="#categories" className="btn-ghost">
</Link>
<Link href="/about" className="btn-ghost">
بیشتر بدانید
</a>
</Link>
</div>
</div>
</div>
+96 -38
View File
@@ -1,46 +1,104 @@
import Image from "next/image";
import { articles } from "@/data/home";
import Link from "next/link";
import { resolveBlogImage } from "@/lib/api/blogs";
import type { BlogPost } from "@/lib/api/types";
export function Articles() {
type ArticlesProps = {
items: BlogPost[];
total?: number;
showHeader?: boolean;
showMoreLink?: boolean;
};
export function Articles({
items,
total = 0,
showHeader = true,
showMoreLink = true,
}: ArticlesProps) {
return (
<section id="articles" className="section bg-surface">
<div className="container-page">
<div className="section-head">
<p className="section-kicker">Insights</p>
<h2 className="section-title">مقالات</h2>
<p className="section-desc">
نکات تخصصی برای خرید هوشمندانه و ارتقاء خطوط تولید صنعتی.
</p>
</div>
<section id="articles" className={showHeader ? "section bg-surface" : undefined}>
<div className={showHeader ? "container-page" : undefined}>
{showHeader ? (
<div className="section-head">
<p className="section-kicker">Insights</p>
<h2 className="section-title">مقالات</h2>
<p className="section-desc">
نکات تخصصی برای خرید هوشمندانه و ارتقاء خطوط تولید صنعتی.
</p>
</div>
) : null}
{items.length === 0 ? (
<div className="border border-line bg-background px-6 py-14 text-center">
<p className="text-base font-bold text-brand-ink">
هنوز مقالهای منتشر نشده است.
</p>
<p className="mt-2 text-sm text-muted">
بهمحض انتشار مطالب جدید، در این بخش نمایش داده میشوند.
</p>
<Link href="/blog" className="btn-primary mt-6 inline-flex">
مشاهده بلاگ
</Link>
</div>
) : (
<>
<div className="grid gap-5 md:grid-cols-3">
{items.map((article) => {
const image = resolveBlogImage(article);
const href = `/blog/${article.slug}`;
<div className="grid gap-5 md:grid-cols-3">
{articles.map((article) => (
<a
key={article.id}
href={article.href}
className="group block overflow-hidden border border-line transition hover:border-brand/40"
>
<div className="relative aspect-[16/10] overflow-hidden bg-[#e8eef4]">
<Image
src={article.image}
alt={article.title}
fill
className="object-cover transition duration-500 group-hover:scale-[1.04]"
sizes="(max-width: 768px) 100vw, 33vw"
/>
return (
<Link
key={article.id}
href={href}
className="group block overflow-hidden border border-line transition hover:border-brand/40"
>
<div className="relative aspect-[16/10] overflow-hidden bg-[#e8eef4]">
{image ? (
<Image
src={image}
alt={article.title}
fill
unoptimized
className="object-cover transition duration-500 group-hover:scale-[1.04]"
sizes="(max-width: 768px) 100vw, 33vw"
/>
) : (
<div className="absolute inset-0 bg-[linear-gradient(135deg,#d7e0ea,#eef3f8)]" />
)}
</div>
<div className="p-5">
{article.categoryName ? (
<p className="mb-2 text-xs font-bold text-brand">
{article.categoryName}
</p>
) : null}
<h3 className="text-base font-extrabold leading-7 text-brand-ink transition group-hover:text-brand">
{article.title}
</h3>
{article.abstract ? (
<p className="mt-2 line-clamp-2 text-sm text-muted">
{article.abstract}
</p>
) : null}
<span className="mt-4 inline-flex items-center gap-2 text-sm font-bold text-brand">
بیشتر بخوانید
<span aria-hidden></span>
</span>
</div>
</Link>
);
})}
</div>
{showMoreLink ? (
<div className="mt-8 text-center">
<Link href="/blog" className="btn-primary">
{total > items.length ? "مقالات بیشتر" : "همه مقالات"}
</Link>
</div>
<div className="p-5">
<h3 className="text-base font-extrabold leading-7 text-brand-ink transition group-hover:text-brand">
{article.title}
</h3>
<span className="mt-4 inline-flex items-center gap-2 text-sm font-bold text-brand">
بیشتر بخوانید
<span aria-hidden></span>
</span>
</div>
</a>
))}
</div>
) : null} </>
)}
</div>
</section>
);
+7 -6
View File
@@ -1,4 +1,5 @@
import Image from "next/image";
import Link from "next/link";
import { categories } from "@/data/home";
export function Categories() {
@@ -13,17 +14,17 @@ export function Categories() {
مسیر سریع به موجودی نو، کارکرده، خطوط سیم و کابل و محتوای آموزشی.
</p>
</div>
<a
href="#products"
<Link
href="/categories"
className="font-industrial text-sm tracking-[0.12em] text-brand transition hover:text-brand-deep"
>
VIEW STOCK
</a>
VIEW ALL
</Link>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{categories.map((cat, index) => (
<a
<Link
key={cat.id}
href={cat.href}
className="group relative block min-h-[22rem] overflow-hidden bg-brand-ink"
@@ -44,7 +45,7 @@ export function Categories() {
<h3 className="mt-1 text-xl font-extrabold">{cat.title}</h3>
<p className="mt-2 text-sm text-white/75">{cat.description}</p>
</div>
</a>
</Link>
))}
</div>
</div>
+271
View File
@@ -0,0 +1,271 @@
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import {
buildCategoryTree,
categoryLabel,
findCategoryById,
} from "@/lib/api/categories";
import type { Category, CategoryTreeNode } from "@/lib/api/types";
type CategoryTreeSelectProps = {
categories: Category[];
name?: string;
defaultValue?: string;
placeholder?: string;
};
function filterTree(
nodes: CategoryTreeNode[],
query: string,
): CategoryTreeNode[] {
if (!query) return nodes;
const q = query.trim().toLowerCase();
const walk = (list: CategoryTreeNode[]): CategoryTreeNode[] => {
const result: CategoryTreeNode[] = [];
for (const node of list) {
const label = categoryLabel(node).toLowerCase();
const en = node.name.toLowerCase();
const children = walk(node.children);
if (label.includes(q) || en.includes(q) || children.length > 0) {
result.push({ ...node, children });
}
}
return result;
};
return walk(nodes);
}
function TreeNodes({
nodes,
depth,
selectedId,
onSelect,
expanded,
toggle,
forceExpand,
}: {
nodes: CategoryTreeNode[];
depth: number;
selectedId: string;
onSelect: (id: string, label: string) => void;
expanded: Set<string>;
toggle: (id: string) => void;
forceExpand: boolean;
}) {
return (
<ul className={depth === 0 ? "space-y-0.5" : "mt-0.5 space-y-0.5"}>
{nodes.map((node) => {
const hasChildren = node.children.length > 0;
const isOpen = forceExpand || expanded.has(node.id);
const label = categoryLabel(node);
const selected = selectedId === node.id;
return (
<li key={node.id}>
<div
className={`flex items-center gap-1 rounded-lg ${
selected ? "bg-brand/10" : "hover:bg-[#eef4fa]"
}`}
style={{ paddingInlineStart: `${depth * 0.85}rem` }}
>
{hasChildren ? (
<button
type="button"
aria-label={isOpen ? "بستن" : "باز کردن"}
onClick={() => toggle(node.id)}
className="grid h-8 w-8 shrink-0 place-items-center rounded-md text-muted transition hover:bg-white hover:text-brand"
>
<span
className={`block text-xs transition ${isOpen ? "rotate-90" : ""}`}
aria-hidden
>
</span>
</button>
) : (
<span className="inline-block w-8 shrink-0" />
)}
<button
type="button"
onClick={() => onSelect(node.id, label)}
className={`min-h-9 flex-1 rounded-lg px-2 py-1.5 text-right text-sm transition ${
selected
? "font-bold text-brand"
: "font-medium text-brand-ink"
}`}
>
{label}
</button>
</div>
{hasChildren && isOpen ? (
<TreeNodes
nodes={node.children}
depth={depth + 1}
selectedId={selectedId}
onSelect={onSelect}
expanded={expanded}
toggle={toggle}
forceExpand={forceExpand}
/>
) : null}
</li>
);
})}
</ul>
);
}
export function CategoryTreeSelect({
categories,
name = "categoryId",
defaultValue = "",
placeholder = "دسته‌بندی",
}: CategoryTreeSelectProps) {
const rootRef = useRef<HTMLDivElement>(null);
const tree = useMemo(() => buildCategoryTree(categories), [categories]);
const initial = findCategoryById(categories, defaultValue);
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const [selectedId, setSelectedId] = useState(defaultValue);
const [selectedLabel, setSelectedLabel] = useState(
initial ? categoryLabel(initial) : "",
);
const [expanded, setExpanded] = useState<Set<string>>(() => {
const ids = new Set<string>();
if (defaultValue) {
let current = findCategoryById(categories, defaultValue);
while (current?.parentId) {
ids.add(current.parentId);
current = findCategoryById(categories, current.parentId);
}
}
for (const root of tree) ids.add(root.id);
return ids;
});
const filtered = useMemo(() => filterTree(tree, query), [tree, query]);
const searching = query.trim().length > 0;
useEffect(() => {
const onPointerDown = (event: MouseEvent) => {
if (!rootRef.current?.contains(event.target as Node)) {
setOpen(false);
}
};
document.addEventListener("mousedown", onPointerDown);
return () => document.removeEventListener("mousedown", onPointerDown);
}, []);
const toggle = (id: string) => {
setExpanded((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
const onSelect = (id: string, label: string) => {
setSelectedId(id);
setSelectedLabel(label);
setQuery("");
setOpen(false);
};
const clear = () => {
setSelectedId("");
setSelectedLabel("");
setQuery("");
};
return (
<div ref={rootRef} className="relative min-w-[14rem]">
<input type="hidden" name={name} value={selectedId} />
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="field-control flex w-full min-h-12 items-center justify-between gap-3 text-right"
>
<span
className={selectedLabel ? "font-bold text-brand-ink" : "text-muted"}
>
{selectedLabel || placeholder}
</span>
<span className="flex items-center gap-2">
{selectedId ? (
<span
role="button"
tabIndex={0}
onClick={(e) => {
e.stopPropagation();
clear();
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
e.stopPropagation();
clear();
}
}}
className="rounded-md px-1.5 py-0.5 text-xs text-muted hover:bg-white hover:text-brand"
>
پاک کردن
</span>
) : null}
<span aria-hidden className="text-xs text-muted">
{open ? "▴" : "▾"}
</span>
</span>
</button>
{open ? (
<div className="absolute inset-x-0 top-[calc(100%+0.4rem)] z-40 overflow-hidden rounded-xl border border-line bg-surface shadow-[0_18px_50px_rgba(6,35,63,0.16)]">
<div className="border-b border-line p-2">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="جستجوی دسته..."
className="field-control min-h-10 w-full"
autoFocus
/>
</div>
<div className="max-h-72 overflow-y-auto p-2">
<button
type="button"
onClick={() => {
clear();
setOpen(false);
}}
className={`mb-1 flex min-h-9 w-full items-center rounded-lg px-3 text-sm transition hover:bg-[#eef4fa] ${
!selectedId ? "font-bold text-brand" : "text-brand-ink"
}`}
>
همه دستهها
</button>
{filtered.length === 0 ? (
<p className="px-3 py-6 text-center text-sm text-muted">
دستهای یافت نشد
</p>
) : (
<TreeNodes
nodes={filtered}
depth={0}
selectedId={selectedId}
onSelect={onSelect}
expanded={expanded}
toggle={toggle}
forceExpand={searching}
/>
)}
</div>
</div>
) : null}
</div>
);
}
+118
View File
@@ -0,0 +1,118 @@
"use client";
import { FormEvent, useState } from "react";
import { submitContact } from "@/lib/api/contact";
import { ApiError } from "@/lib/api/client";
export function ContactForm() {
const [pending, setPending] = useState(false);
const [success, setSuccess] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setPending(true);
setSuccess(null);
setError(null);
const form = event.currentTarget;
const data = new FormData(form);
try {
const result = await submitContact({
title: String(data.get("title") || "").trim(),
name: String(data.get("name") || "").trim(),
email: String(data.get("email") || "").trim() || undefined,
cellNumber: String(data.get("cellNumber") || "").trim() || undefined,
text: String(data.get("text") || "").trim(),
});
setSuccess(result.message || "پیام شما با موفقیت ارسال شد.");
form.reset();
} catch (err) {
setError(
err instanceof ApiError
? err.message
: "ارسال پیام با خطا مواجه شد. دوباره تلاش کنید.",
);
} finally {
setPending(false);
}
};
return (
<form onSubmit={onSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<label className="block space-y-2 text-sm">
<span className="font-bold text-brand-ink">نام</span>
<input
name="name"
required
className="field-control min-h-12 w-full"
placeholder="نام و نام خانوادگی"
/>
</label>
<label className="block space-y-2 text-sm">
<span className="font-bold text-brand-ink">موضوع</span>
<input
name="title"
required
className="field-control min-h-12 w-full"
placeholder="موضوع پیام"
/>
</label>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<label className="block space-y-2 text-sm">
<span className="font-bold text-brand-ink">ایمیل</span>
<input
name="email"
type="email"
className="field-control min-h-12 w-full"
placeholder="email@example.com"
dir="ltr"
/>
</label>
<label className="block space-y-2 text-sm">
<span className="font-bold text-brand-ink">شماره موبایل</span>
<input
name="cellNumber"
className="field-control min-h-12 w-full"
placeholder="+98912..."
dir="ltr"
/>
</label>
</div>
<label className="block space-y-2 text-sm">
<span className="font-bold text-brand-ink">پیام</span>
<textarea
name="text"
required
rows={6}
className="field-control w-full resize-y"
placeholder="متن پیام خود را بنویسید..."
/>
</label>
{success ? (
<p className="rounded-xl border border-brand/30 bg-brand/5 px-4 py-3 text-sm font-bold text-brand">
{success}
</p>
) : null}
{error ? (
<p className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm font-bold text-red-700">
{error}
</p>
) : null}
<button
type="submit"
disabled={pending}
className="btn-primary min-h-12 px-8 disabled:opacity-60"
>
{pending ? "در حال ارسال..." : "ارسال پیام"}
</button>
</form>
);
}
+14 -6
View File
@@ -3,16 +3,24 @@
import { useEffect, useState } from "react";
import { navItems, site } from "@/data/home";
export function Header() {
type HeaderProps = {
forceSolid?: boolean;
};
export function Header({ forceSolid = false }: HeaderProps) {
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [scrolled, setScrolled] = useState(forceSolid);
useEffect(() => {
if (forceSolid) {
setScrolled(true);
return;
}
const onScroll = () => setScrolled(window.scrollY > 24);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
}, [forceSolid]);
useEffect(() => {
document.body.style.overflow = open ? "hidden" : "";
@@ -24,7 +32,7 @@ export function Header() {
return (
<header
className={`fixed inset-x-0 top-0 z-50 transition-colors duration-300 ${
scrolled || open
scrolled || open || forceSolid
? "border-b border-white/10 bg-brand-ink/95 backdrop-blur-md"
: "bg-transparent"
}`}
@@ -63,12 +71,12 @@ export function Header() {
{site.phoneDisplay}
</a>
<a
href={site.loginUrl}
href={site.customerLoginUrl}
className="btn-primary !min-h-10 !px-4 !text-sm"
target="_blank"
rel="noopener noreferrer"
>
ورود فروشندگان
ورود
</a>
</div>
+14 -7
View File
@@ -2,16 +2,18 @@
import Image from "next/image";
import { FormEvent, useState } from "react";
import { useRouter } from "next/navigation";
import { searchSuggestions, site } from "@/data/home";
export function Hero() {
const router = useRouter();
const [query, setQuery] = useState("");
const onSubmit = (e: FormEvent) => {
e.preventDefault();
const q = query.trim();
if (!q) return;
window.location.hash = `products`;
router.push(`/user-products?q=${encodeURIComponent(q)}`);
};
return (
@@ -49,7 +51,7 @@ export function Hero() {
<form
onSubmit={onSubmit}
className="animate-rise animate-rise-delay-3 search-shell mt-8 max-w-3xl border border-white/15 bg-white/95 p-2 shadow-[0_20px_60px_rgba(0,0,0,0.28)] backdrop-blur-sm"
className="animate-rise animate-rise-delay-3 search-shell mt-8 max-w-3xl rounded-2xl p-2"
>
<div className="flex flex-col gap-2 sm:flex-row sm:items-stretch">
<label className="sr-only" htmlFor="machine-search">
@@ -61,22 +63,27 @@ export function Hero() {
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="نام کالای مورد نظر خود را جستجو کنید"
className="min-h-14 flex-1 bg-transparent px-4 text-base text-brand-ink outline-none placeholder:text-[#8a97a6]"
className="min-h-14 flex-1 rounded-xl bg-transparent px-4 text-base text-brand-ink outline-none placeholder:text-[#8a97a6]"
required
/>
<button type="submit" className="btn-primary min-h-14 px-8">
<button
type="submit"
className="btn-primary min-h-14 rounded-xl px-8"
>
جستجو
</button>
</div>
</form>
<div className="mt-4 flex flex-wrap gap-2">
{searchSuggestions.map((item) => (
<button
key={item}
type="button"
onClick={() => setQuery(item)}
className="border border-white/20 bg-white/5 px-3 py-1.5 text-xs text-white/80 transition hover:border-brand hover:bg-brand/20 hover:text-white"
onClick={() => {
setQuery(item);
router.push(`/user-products?q=${encodeURIComponent(item)}`);
}}
className="border border-white/20 bg-white/5 px-3 py-1.5 text-xs text-white/80 transition hover:border-brand hover:bg-brand/20 hover:text-white rounded-lg"
>
{item}
</button>
+38
View File
@@ -0,0 +1,38 @@
import Image from "next/image";
type PageBannerProps = {
kicker: string;
title: string;
description?: string;
};
export function PageBanner({ kicker, title, description }: PageBannerProps) {
return (
<section className="relative overflow-hidden bg-brand-ink text-white">
<Image
src="/images/hero/search-bg.jpg"
alt=""
fill
priority
className="object-cover object-[center_40%]"
sizes="100vw"
/>
<div className="absolute inset-0 bg-brand-ink/80" />
<div className="absolute inset-0 bg-brand/35" />
<div className="relative z-10 container-page py-9 md:py-10">
<p className="font-industrial mb-2 text-[0.72rem] tracking-[0.18em] text-white/80 uppercase">
{kicker}
</p>
<h1 className="max-w-3xl text-3xl font-extrabold leading-tight text-white md:text-[2.35rem]">
{title}
</h1>
{description ? (
<p className="mt-2 max-w-2xl text-sm leading-7 text-white/70 md:text-[0.95rem]">
{description}
</p>
) : null}
</div>
</section>
);
}
+128 -55
View File
@@ -1,64 +1,137 @@
import Image from "next/image";
import { products } from "@/data/home";
import Link from "next/link";
import {
formatCondition,
formatPrice,
productCategory,
productTitle,
} from "@/lib/api/user-products";
import type { UserProductListItem } from "@/lib/api/types";
export function Products() {
type ProductsProps = {
items: UserProductListItem[];
total?: number;
query?: string;
showHeader?: boolean;
showMoreLink?: boolean;
};
export function Products({
items,
total = 0,
query,
showHeader = true,
showMoreLink = true,
}: ProductsProps) {
return (
<section id="products" className="section">
<div className="container-page">
<div className="section-head">
<p className="section-kicker">Stock Machines</p>
<h2 className="section-title">محصولات موجود</h2>
<p className="section-desc">
منتخب ماشینآلات سنگین و صنعتی از فروشگاه ماشینیفای؛ برای جزئیات و
قیمت، استعلام بگیرید.
</p>
</div>
<section id="products" className={showHeader ? "section" : undefined}>
<div className={showHeader ? "container-page" : undefined}>
{showHeader ? (
<div className="section-head">
<p className="section-kicker">Stock Machines</p>
<h2 className="section-title">محصولات موجود</h2>
<p className="section-desc">
{query
? `نتایج جستجو برای «${query}»`
: "منتخب ماشین‌آلات سنگین و صنعتی از فروشگاه ماشینیفای؛ برای جزئیات و قیمت، استعلام بگیرید."}
</p>
</div>
) : query ? (
<p className="mb-6 text-sm text-muted">نتایج جستجو برای «{query}»</p>
) : null}
{items.length === 0 ? (
<div className="border border-line bg-surface px-6 py-14 text-center">
<p className="text-base font-bold text-brand-ink">
در حال حاضر محصول منتشرشدهای یافت نشد.
</p>
<p className="mt-2 text-sm text-muted">
بهمحض انتشار آگهیهای جدید، اینجا نمایش داده میشوند.
</p>
<Link href="/user-products" className="btn-primary mt-6 inline-flex">
مشاهده همه محصولات
</Link>
</div>
) : (
<>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
{items.map((product) => {
const title = productTitle(product);
const category = productCategory(product);
const condition = formatCondition(product.condition);
const price = formatPrice(product.price, product.priceCurrency);
const href = `/user-products/${product.slug}`;
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
{products.map((product) => (
<article
key={product.id}
className="group flex h-full flex-col overflow-hidden border border-line bg-surface transition duration-300 hover:-translate-y-1 hover:border-brand/40"
>
<a href={product.href} className="relative block aspect-[4/3] overflow-hidden bg-[#e8eef4]">
<Image
src={product.image}
alt={product.title}
fill
className="object-cover transition duration-500 group-hover:scale-[1.04]"
sizes="(max-width: 768px) 100vw, 25vw"
/>
<span className="absolute left-3 top-3 bg-brand-ink/90 px-2.5 py-1 text-xs text-white">
{product.condition}
</span>
</a>
<div className="flex flex-1 flex-col p-4">
<p className="font-industrial text-[0.68rem] tracking-[0.14em] text-brand">
{product.titleEn}
</p>
<h3 className="mt-1 text-base font-extrabold leading-7 text-brand-ink">
<a href={product.href}>{product.title}</a>
</h3>
<p className="mt-2 text-sm text-muted">{product.category}</p>
<div className="mt-auto pt-4">
<a
href="#contact"
className="inline-flex w-full items-center justify-center border border-brand px-3 py-2.5 text-sm font-bold text-brand transition hover:bg-brand hover:text-white"
return (
<article
key={product.id}
className="group flex h-full flex-col overflow-hidden border border-line bg-surface transition duration-300 hover:-translate-y-1 hover:border-brand/40"
>
استعلام از فروشنده
</a>
</div>
</div>
</article>
))}
</div>
<Link
href={href}
className="relative block aspect-[4/3] overflow-hidden bg-[#e8eef4]"
>
{product.imageUrl ? (
<Image
src={product.imageUrl}
alt={title}
fill
unoptimized
className="object-cover transition duration-500 group-hover:scale-[1.04]"
sizes="(max-width: 768px) 100vw, 25vw"
/>
) : (
<div className="absolute inset-0 bg-[linear-gradient(135deg,#d7e0ea,#eef3f8)]" />
)}
{condition ? (
<span className="absolute left-3 top-3 bg-brand-ink/90 px-2.5 py-1 text-xs text-white">
{condition}
</span>
) : null}
{product.promoted ? (
<span className="absolute right-3 top-3 bg-brand px-2.5 py-1 text-xs text-white">
ویژه
</span>
) : null}
</Link>
<div className="mt-8 text-center">
<a href="#contact" className="btn-primary">
محصولات بیشتر
</a>
</div>
<div className="flex flex-1 flex-col p-4">
{product.titleEn ? (
<p className="font-industrial text-[0.68rem] tracking-[0.14em] text-brand">
{product.titleEn}
</p>
) : null}
<h3 className="mt-1 text-base font-extrabold leading-7 text-brand-ink">
<Link href={href}>{title}</Link>
</h3>
{category ? (
<p className="mt-2 text-sm text-muted">{category}</p>
) : null}
{price ? (
<p
dir="ltr"
className="font-industrial mt-2 w-full text-left text-sm font-bold tracking-wide text-brand-ink"
>
{price}
</p>
) : null}
<div className="mt-auto pt-4">
<Link href={href} className="btn-secondary w-full !min-h-11 text-sm">
جزئیات و استعلام
</Link> </div>
</div>
</article>
);
})}
</div>
{showMoreLink ? (
<div className="mt-8 text-center">
<Link href="/user-products" className="btn-primary">
{total > items.length ? "محصولات بیشتر" : "همه محصولات"}
</Link>
</div>
) : null} </>
)}
</div>
</section>
);
+53
View File
@@ -0,0 +1,53 @@
import Image from "next/image";
import { site } from "@/data/home";
export function SellCta() {
return (
<section
id="sell"
className="relative overflow-hidden bg-brand-ink text-white"
>
<Image
src="/images/categories/used.jpg"
alt=""
fill
className="object-cover object-center opacity-40"
sizes="100vw"
/>
<div className="absolute inset-0 bg-[linear-gradient(100deg,rgba(6,35,63,0.96)_0%,rgba(6,35,63,0.82)_48%,rgba(0,109,212,0.55)_100%)]" />
<div className="absolute -left-24 top-1/2 h-72 w-72 -translate-y-1/2 rounded-full bg-brand/25 blur-3xl" />
<div className="relative z-10">
<div className="container-page flex flex-col items-start gap-8 py-16 md:flex-row md:items-end md:justify-between md:py-20">
<div className="max-w-3xl">
<p className="font-industrial text-sm tracking-[0.22em] text-[#7ec2ff]">
SELL ON MASHINIFY
</p>
<h2 className="mt-4 text-[clamp(1.7rem,3.4vw,2.75rem)] font-extrabold leading-[1.35]">
ماشین آلات خود را برای فروش در ماشینیفای قرار دهید
</h2>
<p className="mt-4 max-w-xl text-base text-white/75 md:text-lg">
به جمع فروشندگان صنعتی بپیوندید و دستگاههای نو یا کارکرده خود را
مستقیم به خریداران واقعی معرفی کنید.
</p>
</div>
<a
href={site.sellStartUrl}
target="_blank"
rel="noopener noreferrer"
className="group inline-flex min-h-14 items-center gap-3 bg-brand px-8 text-base font-extrabold text-white transition hover:bg-brand-deep"
>
شروع کن
<span
aria-hidden
className="font-industrial text-lg transition-transform duration-300 group-hover:-translate-x-1"
>
</span>
</a>
</div>
</div>
</section>
);
}
+36
View File
@@ -0,0 +1,36 @@
import { Footer } from "@/components/Footer";
import { Header } from "@/components/Header";
import { PageBanner } from "@/components/PageBanner";
type SiteShellProps = {
children: React.ReactNode;
solidHeader?: boolean;
banner?: {
kicker: string;
title: string;
description?: string;
};
};
export function SiteShell({
children,
solidHeader = true,
banner,
}: SiteShellProps) {
return (
<>
<Header forceSolid={solidHeader} />
<main className={solidHeader ? "pt-[4.5rem]" : undefined}>
{banner ? (
<PageBanner
kicker={banner.kicker}
title={banner.title}
description={banner.description}
/>
) : null}
{children}
</main>
<Footer />
</>
);
}