-
-
Stock Machines
-
محصولات موجود
-
- منتخب ماشینآلات سنگین و صنعتی از فروشگاه ماشینیفای؛ برای جزئیات و
- قیمت، استعلام بگیرید.
-
-
+
+
+ {showHeader ? (
+
+
Stock Machines
+
محصولات موجود
+
+ {query
+ ? `نتایج جستجو برای «${query}»`
+ : "منتخب ماشینآلات سنگین و صنعتی از فروشگاه ماشینیفای؛ برای جزئیات و قیمت، استعلام بگیرید."}
+
+
+ ) : query ? (
+
نتایج جستجو برای «{query}»
+ ) : null}
+ {items.length === 0 ? (
+
+
+ در حال حاضر محصول منتشرشدهای یافت نشد.
+
+
+ بهمحض انتشار آگهیهای جدید، اینجا نمایش داده میشوند.
+
+
+ مشاهده همه محصولات
+
+
+ ) : (
+ <>
+
+ {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}`;
-
+
+ {product.imageUrl ? (
+
+ ) : (
+
+ )}
+ {condition ? (
+
+ {condition}
+
+ ) : null}
+ {product.promoted ? (
+
+ ویژه
+
+ ) : null}
+
-
+
+ {product.titleEn ? (
+
+ {product.titleEn}
+
+ ) : null}
+
+ {title}
+
+ {category ? (
+
{category}
+ ) : null}
+ {price ? (
+
+ {price}
+
+ ) : null}
+
+
+ جزئیات و استعلام
+
+
+
+ );
+ })}
+
+
+ {showMoreLink ? (
+
+
+ {total > items.length ? "محصولات بیشتر" : "همه محصولات"}
+
+
+ ) : null} >
+ )}
);
diff --git a/src/components/SellCta.tsx b/src/components/SellCta.tsx
new file mode 100644
index 0000000..0fd63f4
--- /dev/null
+++ b/src/components/SellCta.tsx
@@ -0,0 +1,53 @@
+import Image from "next/image";
+import { site } from "@/data/home";
+
+export function SellCta() {
+ return (
+
+
+
+
+
+
+
+
+
+ SELL ON MASHINIFY
+
+
+ ماشین آلات خود را برای فروش در ماشینیفای قرار دهید
+
+
+ به جمع فروشندگان صنعتی بپیوندید و دستگاههای نو یا کارکرده خود را
+ مستقیم به خریداران واقعی معرفی کنید.
+
+
+
+
+ شروع کن
+
+ ←
+
+
+
+
+
+ );
+}
diff --git a/src/components/SiteShell.tsx b/src/components/SiteShell.tsx
new file mode 100644
index 0000000..11bd9b1
--- /dev/null
+++ b/src/components/SiteShell.tsx
@@ -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 (
+ <>
+
+
+ {banner ? (
+
+ ) : null}
+ {children}
+
+
+ >
+ );
+}
diff --git a/src/data/featured-categories.ts b/src/data/featured-categories.ts
new file mode 100644
index 0000000..ddc2caa
--- /dev/null
+++ b/src/data/featured-categories.ts
@@ -0,0 +1,42 @@
+/**
+ * Curated homepage/categories showcase tiles.
+ * IDs come from GET /tenants/{domain}/categories?entityType=product
+ */
+export const featuredCategoryTiles = [
+ {
+ id: "52",
+ image: "/images/products/fiber-laser.jpg",
+ },
+ {
+ id: "54",
+ image: "/images/products/wire-extruder.jpg",
+ },
+ {
+ id: "60",
+ image: "/images/products/teflon-extruder.jpg",
+ },
+ {
+ id: "61",
+ image: "/images/categories/wire-cable.jpg",
+ },
+ {
+ id: "48",
+ image: "/images/products/granulator.jpg",
+ },
+ {
+ id: "49",
+ image: "/images/categories/used.jpg",
+ },
+ {
+ id: "57",
+ image: "/images/categories/new.jpg",
+ },
+ {
+ id: "51",
+ image: "/images/articles/smart-machines.jpg",
+ },
+ {
+ id: "56",
+ image: "/images/articles/upgrade.jpg",
+ },
+] as const;
diff --git a/src/data/home.ts b/src/data/home.ts
index 3ebf883..90fe576 100644
--- a/src/data/home.ts
+++ b/src/data/home.ts
@@ -8,18 +8,20 @@ export const site = {
phoneDisplay: "025-36700000",
address: "قم، سالاریه، میدان پیچک، ساختمان بنفشه",
loginUrl: "https://app.mashinify.com/auth/login",
+ customerLoginUrl: "https://customer.mashinify.com/login",
+ sellStartUrl:
+ "https://customer.mashinify.com/login?redirect=%2Fmy-products%2Fnew",
supportLabel: "پشتیبانی",
};
export const navItems = [
{ label: "خانه", href: "/" },
- { label: "محصولات", href: "#products" },
- { label: "دستهبندیها", href: "#categories" },
- { label: "مقالات", href: "#articles" },
- { label: "درباره ما", href: "#about" },
- { label: "تماس با ما", href: "#contact" },
+ { label: "محصولات", href: "/user-products" },
+ { label: "دستهبندیها", href: "/categories" },
+ { label: "مقالات", href: "/blog" },
+ { label: "درباره ما", href: "/about" },
+ { label: "تماس با ما", href: "/contact" },
];
-
export const searchSuggestions = [
"لیزر فایبر",
"اکسترودر سیم",
@@ -35,7 +37,7 @@ export const categories = [
titleEn: "Used Products",
description: "ماشینآلات دستدوم بازرسیشده برای تولید مقرونبهصرفه",
image: "/images/categories/used.jpg",
- href: "#products",
+ href: "/user-products?condition=stock",
},
{
id: "new",
@@ -43,7 +45,7 @@ export const categories = [
titleEn: "New Products",
description: "دستگاههای نو با گارانتی و پشتیبانی فروشنده",
image: "/images/categories/new.jpg",
- href: "#products",
+ href: "/user-products?condition=new",
},
{
id: "wire-cable",
@@ -51,7 +53,7 @@ export const categories = [
titleEn: "Wire & Cable",
description: "اکسترودر، مولتیوایر و خطوط تولید سیم و کابل",
image: "/images/categories/wire-cable.jpg",
- href: "#products",
+ href: "/categories",
},
{
id: "education",
@@ -59,50 +61,7 @@ export const categories = [
titleEn: "Education",
description: "راهنما و مقالات تخصصی انتخاب و نگهداری دستگاه",
image: "/images/categories/education.jpg",
- href: "#articles",
- },
-];
-
-export const products = [
- {
- id: 21447,
- title: "دستگاه لیزر فایبر برش فلزات",
- titleEn: "Fiber Laser Cutting Machine",
- category: "ماشینآلات برش",
- condition: "نو",
- image: "/images/products/fiber-laser.jpg",
- href: "#products",
- inquiry: true,
- },
- {
- id: 21446,
- title: "دستگاه گرانولساز",
- titleEn: "Plastic Granulator Machine",
- category: "بازیافت پلاستیک",
- condition: "نو",
- image: "/images/products/granulator.jpg",
- href: "#products",
- inquiry: true,
- },
- {
- id: 21427,
- title: "دستگاه اکسترودر سیم",
- titleEn: "Wire Extruder Machine",
- category: "سیم و کابل",
- condition: "کارکرده",
- image: "/images/products/wire-extruder.jpg",
- href: "#products",
- inquiry: true,
- },
- {
- id: 21426,
- title: "دستگاه اکسترود تفلون",
- titleEn: "Teflon Extruder Machine",
- category: "سیم و کابل",
- condition: "کارکرده",
- image: "/images/products/teflon-extruder.jpg",
- href: "#products",
- inquiry: true,
+ href: "/blog",
},
];
@@ -112,37 +71,14 @@ export const stats = [
{ value: "New & Used", label: "موجودی متنوع" },
];
-export const articles = [
- {
- id: 2898,
- title:
- "مزایای ارتقاء دستگاههای صنعتی قدیمی و تأثیر آن بر افزایش بهرهوری",
- image: "/images/articles/upgrade.jpg",
- href: "#articles",
- },
- {
- id: 2895,
- title:
- "چرا سرمایهگذاری در دستگاههای صنعتی هوشمند، آینده کسبوکار شما را تضمین میکند؟",
- image: "/images/articles/smart-machines.jpg",
- href: "#articles",
- },
- {
- id: 2892,
- title:
- "ماشینآلات صنعتی دست دوم: فرصت هوشمندانه برای تولیدکنندگان با بودجه محدود",
- image: "/images/articles/used-machines.jpg",
- href: "#articles",
- },
-];
-
export const footerLinks = {
pages: [
{ label: "صفحه اصلی", href: "/" },
- { label: "محصولات", href: "#products" },
- { label: "مقالات", href: "#articles" },
- { label: "درباره ما", href: "#about" },
- { label: "تماس با ما", href: "#contact" },
+ { label: "محصولات", href: "/user-products" },
+ { label: "دستهبندیها", href: "/categories" },
+ { label: "مقالات", href: "/blog" },
+ { label: "درباره ما", href: "/about" },
+ { label: "تماس با ما", href: "/contact" },
],
categories: categories.map((c) => ({
label: c.title,
diff --git a/src/lib/api/blogs.ts b/src/lib/api/blogs.ts
new file mode 100644
index 0000000..7543b8f
--- /dev/null
+++ b/src/lib/api/blogs.ts
@@ -0,0 +1,57 @@
+import { apiGet, ApiError } from "./client";
+import type { BlogPost, Paginated } from "./types";
+
+export type ListBlogsParams = {
+ page?: number;
+ pageSize?: number;
+ type?: "news" | "article" | "blog";
+ categoryId?: string;
+ title?: string;
+};
+
+const emptyPage =
(page = 1, pageSize = 12): Paginated => ({
+ items: [],
+ total: 0,
+ page,
+ pageSize,
+});
+
+export async function listBlogs(params: ListBlogsParams = {}) {
+ const page = params.page ?? 1;
+ const pageSize = params.pageSize ?? 12;
+ try {
+ return await apiGet>("/blogs", {
+ page,
+ pageSize,
+ type: params.type,
+ categoryId: params.categoryId,
+ title: params.title,
+ });
+ } catch (error) {
+ console.error("listBlogs failed", error);
+ return emptyPage(page, pageSize);
+ }
+}
+export async function getBlogBySlug(slug: string) {
+ const data = await apiGet<{ blog: BlogPost }>(
+ `/blogs/${encodeURIComponent(slug)}`,
+ );
+ return data.blog;
+}
+
+export async function getBlogBySlugOrNull(slug: string) {
+ try {
+ return await getBlogBySlug(slug);
+ } catch (error) {
+ if (error instanceof ApiError && error.status === 404) return null;
+ throw error;
+ }
+}
+
+/** Prefer title image; otherwise first inside HTML body. */
+export function resolveBlogImage(blog: Pick) {
+ if (blog.titleImageUrl) return blog.titleImageUrl;
+ const html = blog.mainTextHtml ?? "";
+ const match = html.match(/ ]+src=["']([^"']+)["']/i);
+ return match?.[1] ?? null;
+}
diff --git a/src/lib/api/business.ts b/src/lib/api/business.ts
new file mode 100644
index 0000000..863b693
--- /dev/null
+++ b/src/lib/api/business.ts
@@ -0,0 +1,11 @@
+import { apiGet } from "./client";
+import type { BusinessInfo } from "./types";
+
+export async function getBusinessInfo() {
+ try {
+ return await apiGet("/website/business-info");
+ } catch (error) {
+ console.error("getBusinessInfo failed", error);
+ return null;
+ }
+}
diff --git a/src/lib/api/categories.ts b/src/lib/api/categories.ts
new file mode 100644
index 0000000..2785ee4
--- /dev/null
+++ b/src/lib/api/categories.ts
@@ -0,0 +1,50 @@
+import { apiGet } from "./client";
+import type { Category, CategoryTreeNode } from "./types";
+
+export async function listCategories(entityType: "product" | "blog" = "product") {
+ try {
+ const data = await apiGet<{ items: Category[] }>("/categories", {
+ entityType,
+ });
+ return data.items ?? [];
+ } catch (error) {
+ console.error("listCategories failed", error);
+ return [] as Category[];
+ }
+}
+
+export function categoryLabel(category: Pick) {
+ return category.nameFa || category.name;
+}
+
+export function buildCategoryTree(categories: Category[]): CategoryTreeNode[] {
+ const map = new Map();
+ for (const category of categories) {
+ map.set(category.id, { ...category, children: [] });
+ }
+
+ const roots: CategoryTreeNode[] = [];
+ for (const node of map.values()) {
+ if (node.parentId && map.has(node.parentId)) {
+ map.get(node.parentId)!.children.push(node);
+ } else {
+ roots.push(node);
+ }
+ }
+
+ const sortNodes = (nodes: CategoryTreeNode[]) => {
+ nodes.sort(
+ (a, b) =>
+ a.sortOrder - b.sortOrder ||
+ categoryLabel(a).localeCompare(categoryLabel(b), "fa"),
+ );
+ for (const node of nodes) sortNodes(node.children);
+ };
+ sortNodes(roots);
+ return roots;
+}
+
+export function findCategoryById(categories: Category[], id?: string | null) {
+ if (!id) return null;
+ return categories.find((item) => item.id === id) ?? null;
+}
diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts
new file mode 100644
index 0000000..f406730
--- /dev/null
+++ b/src/lib/api/client.ts
@@ -0,0 +1,92 @@
+import { API_BASE_URL, SITE_DOMAIN } from "./config";
+
+export class ApiError extends Error {
+ status: number;
+
+ constructor(message: string, status: number) {
+ super(message);
+ this.name = "ApiError";
+ this.status = status;
+ }
+}
+
+type QueryValue = string | number | boolean | null | undefined;
+
+export function buildTenantUrl(
+ path: string,
+ query?: Record,
+ domain: string = SITE_DOMAIN,
+) {
+ const normalized = path.startsWith("/") ? path : `/${path}`;
+ const url = new URL(`${API_BASE_URL}/tenants/${domain}${normalized}`);
+
+ if (query) {
+ for (const [key, value] of Object.entries(query)) {
+ if (value === undefined || value === null || value === "") continue;
+ url.searchParams.set(key, String(value));
+ }
+ }
+
+ return url;
+}
+
+export async function apiGet(
+ path: string,
+ query?: Record,
+ init?: RequestInit,
+): Promise {
+ const url = buildTenantUrl(path, query);
+ const response = await fetch(url, {
+ ...init,
+ headers: {
+ Accept: "application/json",
+ ...init?.headers,
+ },
+ next: init?.next ?? { revalidate: 60 },
+ });
+
+ if (!response.ok) {
+ let message = `API ${response.status}`;
+ try {
+ const body = (await response.json()) as { message?: string };
+ if (body?.message) message = body.message;
+ } catch {
+ /* ignore */
+ }
+ throw new ApiError(message, response.status);
+ }
+
+ return response.json() as Promise;
+}
+
+export async function apiPost(
+ path: string,
+ body: unknown,
+ init?: RequestInit,
+): Promise {
+ const url = buildTenantUrl(path);
+ const response = await fetch(url, {
+ ...init,
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ ...init?.headers,
+ },
+ body: JSON.stringify(body),
+ cache: "no-store",
+ });
+
+ if (!response.ok) {
+ let message = `API ${response.status}`;
+ try {
+ const data = (await response.json()) as { message?: string };
+ if (data?.message) message = data.message;
+ } catch {
+ /* ignore */
+ }
+ throw new ApiError(message, response.status);
+ }
+
+ return response.json() as Promise;
+}
diff --git a/src/lib/api/config.ts b/src/lib/api/config.ts
new file mode 100644
index 0000000..2dc5ebb
--- /dev/null
+++ b/src/lib/api/config.ts
@@ -0,0 +1,6 @@
+export const API_BASE_URL =
+ process.env.NEXT_PUBLIC_API_BASE_URL ?? "https://api.meshkee.com/api/v1";
+
+/** Website apex host (no www/api/customer/business). */
+export const SITE_DOMAIN =
+ process.env.NEXT_PUBLIC_SITE_DOMAIN ?? "mashinify.com";
diff --git a/src/lib/api/contact.ts b/src/lib/api/contact.ts
new file mode 100644
index 0000000..052b1a9
--- /dev/null
+++ b/src/lib/api/contact.ts
@@ -0,0 +1,9 @@
+import { apiPost } from "./client";
+import type { ContactSubmissionInput } from "./types";
+
+export async function submitContact(input: ContactSubmissionInput) {
+ return apiPost<{ submission?: unknown; message?: string }>(
+ "/contact-submissions",
+ input,
+ );
+}
diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts
new file mode 100644
index 0000000..cbdcef4
--- /dev/null
+++ b/src/lib/api/types.ts
@@ -0,0 +1,134 @@
+export type Paginated = {
+ items: T[];
+ total: number;
+ page: number;
+ pageSize: number;
+};
+
+export type BlogAuthor = {
+ id: string;
+ firstName: string | null;
+ lastName: string | null;
+ email?: string | null;
+};
+
+export type BlogPost = {
+ id: string;
+ businessId: string;
+ title: string;
+ slug: string;
+ type: "news" | "article" | "blog" | string;
+ abstract: string | null;
+ mainTextHtml?: string | null;
+ status: string;
+ categoryId: string | null;
+ categoryName: string | null;
+ tags: string[];
+ authorId: string | null;
+ author: BlogAuthor | null;
+ titleImageUrl: string | null;
+ featuredMediaId: string | null;
+ commentCount: number;
+ publishedAt: string | null;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type UserProductCondition =
+ | "new"
+ | "stock"
+ | "needs_repair"
+ | "scrap"
+ | string;
+
+export type UserProductListItem = {
+ id: string;
+ slug: string;
+ titleFa: string | null;
+ titleEn: string | null;
+ price: number | string | null;
+ priceCurrency: string | null;
+ condition: UserProductCondition | null;
+ cityName?: string | null;
+ cityNameFa?: string | null;
+ countryName?: string | null;
+ countryNameFa?: string | null;
+ imageUrl: string | null;
+ categoryId?: string | null;
+ categoryName?: string | null;
+ categoryNameFa?: string | null;
+ promoted?: boolean;
+ publishedAt?: string | null;
+};
+
+export type UserProductImage = {
+ mediaId: string;
+ url: string;
+};
+
+export type UserProduct = UserProductListItem & {
+ description?: string | null;
+ descriptionFa?: string | null;
+ descriptionEn?: string | null;
+ images?: UserProductImage[];
+ galleryMediaIds?: string[];
+ featuredMediaId?: string | null;
+ countryId?: string | null;
+ cityId?: string | null;
+ countrySlug?: string | null;
+ deliveryNotes?: string | null;
+ technicalNotes?: string | null;
+ technicalValues?: unknown;
+};
+
+export type Category = {
+ id: string;
+ businessId: string;
+ entityType: string;
+ parentId: string | null;
+ name: string;
+ nameFa: string | null;
+ slug: string;
+ description: string | null;
+ sortOrder: number;
+ isActive: boolean;
+ variationCount?: number;
+};
+
+export type CategoryTreeNode = Category & {
+ children: CategoryTreeNode[];
+};
+
+export type BusinessInfo = {
+ id: string;
+ name: string;
+ nameFa: string | null;
+ about: string | null;
+ vision: string | null;
+ logoUrl: string | null;
+ faviconUrl: string | null;
+ emails: string[];
+ phoneNumbers: string[];
+ socialMedia?: {
+ whatsapp?: string;
+ telegram?: string;
+ instagram?: string;
+ linkedin?: string;
+ youtube?: string;
+ aparat?: string;
+ };
+ addresses: Array<{
+ title?: string | null;
+ address?: string | null;
+ cityName?: string | null;
+ cityNameFa?: string | null;
+ }>;
+};
+
+export type ContactSubmissionInput = {
+ title: string;
+ name: string;
+ text: string;
+ email?: string;
+ cellNumber?: string;
+};
diff --git a/src/lib/api/user-products.ts b/src/lib/api/user-products.ts
new file mode 100644
index 0000000..435fd0a
--- /dev/null
+++ b/src/lib/api/user-products.ts
@@ -0,0 +1,99 @@
+import { apiGet, ApiError } from "./client";
+import type {
+ Paginated,
+ UserProduct,
+ UserProductCondition,
+ UserProductListItem,
+} from "./types";
+
+export type ListUserProductsParams = {
+ page?: number;
+ pageSize?: number;
+ q?: string;
+ name?: string;
+ categoryId?: string;
+ cityId?: string;
+ countryId?: string;
+ condition?: UserProductCondition;
+ promoted?: boolean;
+};
+
+const emptyPage = (page = 1, pageSize = 12): Paginated => ({
+ items: [],
+ total: 0,
+ page,
+ pageSize,
+});
+
+export async function listUserProducts(params: ListUserProductsParams = {}) {
+ const page = params.page ?? 1;
+ const pageSize = params.pageSize ?? 12;
+ try {
+ return await apiGet>("/user-products", {
+ page,
+ pageSize,
+ q: params.q,
+ name: params.name,
+ categoryId: params.categoryId,
+ cityId: params.cityId,
+ countryId: params.countryId,
+ condition: params.condition,
+ promoted: params.promoted,
+ });
+ } catch (error) {
+ console.error("listUserProducts failed", error);
+ return emptyPage(page, pageSize);
+ }
+}
+export async function getUserProductBySlug(slug: string) {
+ const data = await apiGet<{ product: UserProduct }>(
+ `/user-products/${encodeURIComponent(slug)}`,
+ );
+ return data.product;
+}
+
+export async function getUserProductBySlugOrNull(slug: string) {
+ try {
+ return await getUserProductBySlug(slug);
+ } catch (error) {
+ if (error instanceof ApiError && error.status === 404) return null;
+ throw error;
+ }
+}
+
+export const conditionLabels: Record = {
+ new: "نو",
+ stock: "کارکرده",
+ needs_repair: "نیازمند تعمیر",
+ scrap: "اسقاط",
+};
+
+export function formatCondition(condition: string | null | undefined) {
+ if (!condition) return null;
+ return conditionLabels[condition] ?? condition;
+}
+
+export function productTitle(product: Pick) {
+ return product.titleFa || product.titleEn || "بدون عنوان";
+}
+
+export function productCategory(
+ product: Pick,
+) {
+ return product.categoryNameFa || product.categoryName || null;
+}
+
+export function formatPrice(
+ price: number | string | null | undefined,
+ currency: string | null | undefined = "IRR",
+) {
+ if (price === null || price === undefined || price === "") return null;
+ const value = typeof price === "string" ? Number(price) : price;
+ if (!Number.isFinite(value)) return String(price);
+
+ const formatted = new Intl.NumberFormat("en-US").format(value);
+ if (!currency || currency === "IRR" || currency === "IRT") {
+ return `${formatted} تومان`;
+ }
+ return `${currency} ${formatted}`;
+}