From 8194b7292fc5300c5886a6eaffad02e138cb60fe Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Mon, 10 Aug 2026 10:54:27 +0330 Subject: [PATCH] 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 --- next.config.ts | 9 +- src/app/about/page.tsx | 76 ++++++++ src/app/blog/[slug]/page.tsx | 107 ++++++++++ src/app/blog/page.tsx | 94 +++++++++ src/app/categories/page.tsx | 83 ++++++++ src/app/contact/page.tsx | 106 ++++++++++ src/app/globals.css | 54 ++++- src/app/page.tsx | 15 +- src/app/user-products/[slug]/page.tsx | 185 ++++++++++++++++++ src/app/user-products/page.tsx | 141 ++++++++++++++ src/components/AboutCta.tsx | 9 +- src/components/Articles.tsx | 134 +++++++++---- src/components/Categories.tsx | 13 +- src/components/CategoryTreeSelect.tsx | 271 ++++++++++++++++++++++++++ src/components/ContactForm.tsx | 118 +++++++++++ src/components/Header.tsx | 20 +- src/components/Hero.tsx | 21 +- src/components/PageBanner.tsx | 38 ++++ src/components/Products.tsx | 183 +++++++++++------ src/components/SellCta.tsx | 53 +++++ src/components/SiteShell.tsx | 36 ++++ src/data/featured-categories.ts | 42 ++++ src/data/home.ts | 98 ++-------- src/lib/api/blogs.ts | 57 ++++++ src/lib/api/business.ts | 11 ++ src/lib/api/categories.ts | 50 +++++ src/lib/api/client.ts | 92 +++++++++ src/lib/api/config.ts | 6 + src/lib/api/contact.ts | 9 + src/lib/api/types.ts | 134 +++++++++++++ src/lib/api/user-products.ts | 99 ++++++++++ 31 files changed, 2160 insertions(+), 204 deletions(-) create mode 100644 src/app/about/page.tsx create mode 100644 src/app/blog/[slug]/page.tsx create mode 100644 src/app/blog/page.tsx create mode 100644 src/app/categories/page.tsx create mode 100644 src/app/contact/page.tsx create mode 100644 src/app/user-products/[slug]/page.tsx create mode 100644 src/app/user-products/page.tsx create mode 100644 src/components/CategoryTreeSelect.tsx create mode 100644 src/components/ContactForm.tsx create mode 100644 src/components/PageBanner.tsx create mode 100644 src/components/SellCta.tsx create mode 100644 src/components/SiteShell.tsx create mode 100644 src/data/featured-categories.ts create mode 100644 src/lib/api/blogs.ts create mode 100644 src/lib/api/business.ts create mode 100644 src/lib/api/categories.ts create mode 100644 src/lib/api/client.ts create mode 100644 src/lib/api/config.ts create mode 100644 src/lib/api/contact.ts create mode 100644 src/lib/api/types.ts create mode 100644 src/lib/api/user-products.ts diff --git a/next.config.ts b/next.config.ts index e9ffa30..1ece0ce 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + remotePatterns: [ + { protocol: "https", hostname: "**.parspack.net" }, + { protocol: "https", hostname: "cdn.meshkee.com" }, + { protocol: "https", hostname: "**.meshkee.com" }, + { protocol: "https", hostname: "api.meshkee.com" }, + ], + }, }; export default nextConfig; diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx new file mode 100644 index 0000000..0937eb2 --- /dev/null +++ b/src/app/about/page.tsx @@ -0,0 +1,76 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import { SiteShell } from "@/components/SiteShell"; +import { getBusinessInfo } from "@/lib/api/business"; +import { site, stats } from "@/data/home"; + +export const metadata: Metadata = { + title: "درباره ما", + description: site.description, +}; + +export default async function AboutPage() { + const info = await getBusinessInfo(); + const name = info?.nameFa || info?.name || site.name; + const about = + info?.about?.trim() || + `${site.description} ماشینیفای بستری برای خرید و فروش امن ماشین‌آلات صنعتی است؛ با تمرکز روی شفافیت، پشتیبانی تخصصی و دسترسی سریع به موجودی نو و کارکرده.`; + const vision = + info?.vision?.trim() || + "ایجاد قابل‌اعتمادترین بازار ماشین‌آلات صنعتی برای تولیدکنندگان، کارگاه‌ها و فروشندگان در ایران."; + + return ( + +
+
+
+ {name} +
+ +
+

Who We Are

+

{name}

+

{about}

+

{vision}

+ +
+ {stats.map((item) => ( +
+
+ {item.value} +
+
+ {item.label} +
+
+ ))} +
+ +
+ + تماس با ما + + + مشاهده محصولات + +
+
+
+
+
+ ); +} diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..1ee507d --- /dev/null +++ b/src/app/blog/[slug]/page.tsx @@ -0,0 +1,107 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { SiteShell } from "@/components/SiteShell"; +import { getBlogBySlugOrNull, resolveBlogImage } from "@/lib/api/blogs"; + +type PageProps = { + params: Promise<{ slug: string }>; +}; + +export async function generateMetadata({ + params, +}: PageProps): Promise { + const { slug } = await params; + const blog = await getBlogBySlugOrNull(slug); + if (!blog) return { title: "مقاله یافت نشد" }; + return { + title: blog.title, + description: blog.abstract ?? blog.title, + }; +} + +export default async function BlogDetailPage({ params }: PageProps) { + const { slug } = await params; + const blog = await getBlogBySlugOrNull(slug); + if (!blog) notFound(); + + const cover = resolveBlogImage(blog); + const authorName = blog.author + ? [blog.author.firstName, blog.author.lastName].filter(Boolean).join(" ") + : null; + const published = blog.publishedAt + ? new Intl.DateTimeFormat("fa-IR", { dateStyle: "medium" }).format( + new Date(blog.publishedAt), + ) + : null; + + return ( + +
+
+
+ + مقالات + + / + {blog.title} +
+ + {blog.categoryName ? ( +

{blog.categoryName}

+ ) : null} +

+ {blog.title} +

+
+ {authorName ? {authorName} : null} + {published ? ( + {published} + ) : null} +
+ + {blog.abstract ? ( +

{blog.abstract}

+ ) : null} + + {cover ? ( +
+ {blog.title} +
+ ) : null} + + {blog.mainTextHtml ? ( +
+ ) : null} + +
+ + بازگشت به مقالات + +
+
+
+
+ ); +} diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx new file mode 100644 index 0000000..38fb52d --- /dev/null +++ b/src/app/blog/page.tsx @@ -0,0 +1,94 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { SiteShell } from "@/components/SiteShell"; +import { Articles } from "@/components/Articles"; +import { listBlogs } from "@/lib/api/blogs"; + +export const metadata: Metadata = { + title: "مقالات", + description: "مقالات و مطالب تخصصی ماشینیفای", +}; + +type PageProps = { + searchParams: Promise<{ page?: string; title?: string; type?: string }>; +}; + +export default async function BlogIndexPage({ searchParams }: PageProps) { + const params = await searchParams; + const page = Number(params.page || "1") || 1; + const title = params.title?.trim() || undefined; + const type = (params.type as "news" | "article" | "blog" | undefined) || undefined; + + const data = await listBlogs({ + page, + pageSize: 12, + title, + type, + }); + + return ( + +
+
+
+ + +
+ + + {data.total > data.pageSize ? ( +
+ {page > 1 ? ( + + قبلی + + ) : null} + + صفحه {page} + + {page * data.pageSize < data.total ? ( + + بعدی + + ) : null} +
+ ) : null} +
+
+
+ ); +} diff --git a/src/app/categories/page.tsx b/src/app/categories/page.tsx new file mode 100644 index 0000000..b5ca015 --- /dev/null +++ b/src/app/categories/page.tsx @@ -0,0 +1,83 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import { SiteShell } from "@/components/SiteShell"; +import { featuredCategoryTiles } from "@/data/featured-categories"; +import { categoryLabel, listCategories } from "@/lib/api/categories"; + +export const metadata: Metadata = { + title: "دسته‌بندی‌ها", + description: "۹ دسته‌بندی منتخب ماشین‌آلات صنعتی از موجودی ماشینیفای", +}; + +export default async function CategoriesPage() { + const categories = await listCategories("product"); + const byId = new Map(categories.map((item) => [item.id, item])); + + const tiles = featuredCategoryTiles + .map((tile) => { + const category = byId.get(tile.id); + if (!category) return null; + return { + ...tile, + category, + title: categoryLabel(category), + titleEn: category.name, + }; + }) + .filter(Boolean) as Array<{ + id: string; + image: string; + title: string; + titleEn: string; + category: { id: string; slug: string }; + }>; + + return ( + +
+
+ {tiles.length === 0 ? ( +
+

+ دسته‌بندی‌ای برای نمایش یافت نشد. +

+
+ ) : ( +
+ {tiles.map((tile) => ( + + {tile.title} +
+
+

+ {tile.titleEn} +

+

{tile.title}

+
+ + ))} +
+ )} +
+
+
+ ); +} diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx new file mode 100644 index 0000000..e6ab1c8 --- /dev/null +++ b/src/app/contact/page.tsx @@ -0,0 +1,106 @@ +import type { Metadata } from "next"; +import { SiteShell } from "@/components/SiteShell"; +import { ContactForm } from "@/components/ContactForm"; +import { getBusinessInfo } from "@/lib/api/business"; +import { site } from "@/data/home"; + +export const metadata: Metadata = { + title: "تماس با ما", + description: "ارسال پیام و راه‌های ارتباطی با ماشینیفای", +}; + +export default async function ContactPage() { + const info = await getBusinessInfo(); + const phones = + info?.phoneNumbers?.filter(Boolean).length + ? info.phoneNumbers + : [site.phoneDisplay]; + const emails = info?.emails?.filter(Boolean) ?? []; + const addresses = + info?.addresses?.length + ? info.addresses + .map((item) => item.address) + .filter(Boolean) + : [site.address]; + + return ( + +
+
+ + +
+

Message

+

+ فرم تماس +

+

+ پیام خود را ارسال کنید؛ در کوتاه‌ترین زمان با شما تماس می‌گیریم. +

+ +
+
+
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 45df7e4..2544a37 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -140,10 +140,11 @@ img { gap: 0.5rem; min-height: 3rem; padding: 0.7rem 1.35rem; + border-radius: 0.75rem; background: var(--brand); color: #fff; font-weight: 700; - transition: background 0.25s ease, transform 0.25s ease; + transition: background 0.2s ease; } .btn-primary:hover { @@ -157,10 +158,11 @@ img { gap: 0.5rem; min-height: 3rem; padding: 0.7rem 1.35rem; + border-radius: 0.75rem; border: 1px solid rgba(255, 255, 255, 0.45); color: #fff; font-weight: 600; - transition: background 0.25s ease, border-color 0.25s ease; + transition: background 0.2s ease, border-color 0.2s ease; } .btn-ghost:hover { @@ -168,6 +170,46 @@ img { border-color: #fff; } +.field-control { + border: 1px solid var(--line); + border-radius: 0.75rem; + background: #fff; + padding: 0.7rem 1rem; + color: var(--foreground); + outline: none; + transition: border-color 0.2s ease; +} + +.field-control:focus, +.field-control:focus-within { + border-color: var(--brand); +} + +.btn-secondary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + min-height: 3rem; + padding: 0.7rem 1.35rem; + border-radius: 0.75rem; + border: 1px solid var(--brand); + color: var(--brand); + font-weight: 700; + background: #fff; + transition: background 0.2s ease, color 0.2s ease; +} + +.btn-secondary:hover { + background: var(--brand); + color: #fff; +} + +.filter-shell { + border: 1px solid var(--line); + border-radius: 1rem; + background: #fff; +} @keyframes rise-in { from { opacity: 0; @@ -205,8 +247,14 @@ img { animation-delay: 0.36s; } +.search-shell { + background: #fff; + border: 1px solid rgba(255, 255, 255, 0.55); + box-shadow: none; +} + .search-shell:focus-within { - animation: search-glow 1.6s ease infinite; + border-color: rgba(255, 255, 255, 0.8); } @media (prefers-reduced-motion: reduce) { diff --git a/src/app/page.tsx b/src/app/page.tsx index b92f7fb..706d063 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,17 +5,26 @@ import { Footer } from "@/components/Footer"; import { Header } from "@/components/Header"; import { Hero } from "@/components/Hero"; import { Products } from "@/components/Products"; +import { SellCta } from "@/components/SellCta"; +import { listBlogs } from "@/lib/api/blogs"; +import { listUserProducts } from "@/lib/api/user-products"; + +export default async function Home() { + const [userProducts, blogs] = await Promise.all([ + listUserProducts({ page: 1, pageSize: 8 }), + listBlogs({ page: 1, pageSize: 3 }), + ]); -export default function Home() { return ( <>
- + + - +