Replace the landing with the artifact design and rebuild the admin in Ant Design.

Adds consultation leads, section editors, and the one-time content reset for this release.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-01 20:36:06 +03:30
co-authored by Cursor
parent 5147c661f0
commit b0088c35d1
130 changed files with 8024 additions and 2801 deletions
+191 -145
View File
@@ -1,180 +1,226 @@
/**
* Lightweight local mock API for UI preview when Postgres isn't configured.
* Serves the same shape as Nest public endpoints from seed defaults.
* Lightweight in-memory mock of the Nest API for UI preview when Postgres isn't configured.
* Serves the public and admin endpoints used by the landing and the admin panel.
* Requires `npm run build:shared` first (reads defaults from packages/shared/dist).
*/
import http from "node:http";
import { createRequire } from "node:module";
const sections = {
hero: {
eyebrow: "گروه توسعه نرم‌افزار",
location: "[ تأسیس ۱۴۰۰ — تهران / دورکاری ]",
loaderTitle: "هوش‌افزار منتک",
lines: [
"در هوش‌افزار منتک",
"بهترین نرم‌افزارها و خدمات",
"متناسب با کسب‌وکار شما پیشنهاد می‌شود",
],
highlightWord: "هوش‌افزار منتک",
highlightWords: ["هوش‌افزار منتک"],
body: "منتک یک تیم مهندسی ارشد است که پلتفرم‌های وب، اپلیکیشن موبایل و زیرساخت ابری را برای کسب‌وکارهایی می‌سازد که نمی‌خواهند معمولی باشند.",
cta: "مشاهده نمونه‌کارها",
badge: "مهندسی منتک • از ۱۴۰۰ •",
coords: "تهران، ایران",
projectsLabel: "بیش از ۱۲۰ پروژه تحویل‌شده",
},
marquee: {
items: [
"آبشده همکار",
"آبشده خانگی",
"فروشگاه آنلاین",
"سامانه بد و بس",
"سامانه فوری سند",
"مدیریت ارتباط با مشتری CRM",
"هوش مصنوعی",
],
},
manifesto: {
tag: "ما که هستیم",
words: [
{ text: "ما" },
{ text: "نرم‌افزاری" },
{ text: "می‌سازیم", volt: true },
{ text: "که" },
{ text: "تفاوت" },
{ text: "ایجاد" },
{ text: "کند." },
{ text: "منتک", volt: true },
],
body: "استراتژی، طراحی و مهندسی زیر یک سقف — با دقت وسواس‌گونه نسبت به کیفیت و عملکرد.",
},
services: {
tag: "چه می‌کنیم",
items: [
{ n: "۰۱", title: "استراتژی محصول", desc: "ایده‌های مبهم را به نقشه راه معتبر تبدیل می‌کنیم.", tags: ["کشف محصول", "پژوهش کاربر", "نقشه راه", "MVP"] },
{ n: "۰۲", title: "مهندسی وب", desc: "پلتفرم‌های وب سریع و مقیاس‌پذیر.", tags: ["Nuxt", "NestJS", "Node.js", "React"] },
{ n: "۰۳", title: "اپلیکیشن موبایل", desc: "تجربهٔ بومی‌گونه روی iOS و Android.", tags: ["React Native", "Flutter"] },
{ n: "۰۴", title: "ابرو DevOps", desc: "استقرار خودکار، امنیت و مانیتورینگ.", tags: ["AWS / GCP", "کوبرنتیز", "CI/CD"] },
{ n: "۰۵", title: "هوش مصنوعی و داده", desc: "هوش مصنوعی کاربردی و خطوط لوله داده.", tags: ["LLM", "اتوماسیون"] },
],
},
work: {
tag: "نمونه کارها",
items: [
{ n: "۰۱", title: "پالس‌بورد", cat: "پلتفرم تحلیلی فین‌تک", year: "۱۴۰۳" },
{ n: "۰۲", title: "ایرو‌لوپ", cat: "نرم‌افزار لجستیک", year: "۱۴۰۳" },
{ n: "۰۳", title: "مد‌سینک", cat: "پلتفرم هلث‌تک", year: "۱۴۰۲" },
{ n: "۰۴", title: "نوواپی", cat: "زیرساخت پرداخت", year: "۱۴۰۲" },
],
},
process: {
tag: "چگونه کار می‌کنیم",
steps: [
{ n: "۰۱", title: "کشف", desc: "کارگاه، پژوهش و هم‌راستایی روی نتایج." },
{ n: "۰۲", title: "طراحی", desc: "سیستم‌ها، جریان‌ها و رابط‌های ساده." },
{ n: "۰۳", title: "توسعه", desc: "مهندسان ارشد و نرم‌افزار آمادهٔ تولید." },
{ n: "۰۴", title: "استقرار و مقیاس", desc: "CI/CD، مانیتورینگ و رشد پایدار." },
],
},
stats: {
items: [
{ value: 120, suffix: "+", label: "پروژه تحویل‌شده" },
{ value: 40, suffix: "+", label: "مشتری" },
{ value: 15, suffix: "", label: "مهندس و طراح" },
{ value: 98, suffix: "%", label: "ماندگاری مشتری" },
],
},
team: {
tag: "تیم",
headline: "با جمع ما آشنا شوید",
body: "تیمی جمع‌وجور از متخصصان محصول، طراحی و مهندسی.",
members: [
{ name: "علی رضایی", role: "مدیرعامل و هم‌بنیان‌گذار" },
{ name: "سارا محمدی", role: "مدیر فنی و هم‌بنیان‌گذار" },
{ name: "مریم حسینی", role: "سرپرست طراحی" },
{ name: "رضا کریمی", role: "مهندس ارشد" },
],
},
footer: {
tag: "تماس",
marquee: ["پروژه‌ای در ذهن دارید؟", "سلام کنید", "با هم کار کنیم"],
cta: "شروع همکاری",
},
};
const require = createRequire(import.meta.url);
const { DEFAULT_SECTIONS, SECTION_KEYS } = require("../packages/shared/dist/index.js");
const site = {
settings: {
brandName: "منتک",
tagline: "گروه توسعه نرم‌افزار",
email: "hello@montech.ir",
phone: "+98 21 0000 0000",
address: "تهران، ایران",
timezone: "Asia/Tehran",
logoUrl: null,
socials: [
{ label: "لینکدین", url: "https://linkedin.com" },
{ label: "گیت‌هاب", url: "https://github.com" },
],
},
sections,
pages: [
{
key: "home",
title: "خانه",
seo: {
title: "منتک — گروه توسعه نرم‌افزار",
description: "منتک پلتفرم‌های وب، اپلیکیشن موبایل و زیرساخت ابری می‌سازد.",
canonical: "http://localhost:3000/",
},
},
let settings = {
id: "default",
brandName: "منتک",
tagline: "هوش‌افزار منظر",
email: "info@montech.ir",
phone: "۰۹۱۲ ۳۴۵ ۶۷۸۹",
address: "تهران، ایران",
timezone: "Asia/Tehran",
logoUrl: null,
socials: [
{ label: "تلگرام", url: "#" },
{ label: "لینکدین", url: "#" },
{ label: "اینستاگرام", url: "#" },
],
};
const blog = [
const sections = Object.fromEntries(SECTION_KEYS.map((k) => [k, structuredClone(DEFAULT_SECTIONS[k])]));
const pages = [
{ id: "p1", key: "home", title: "خانه", seo: { title: "منتک | هوش‌افزار منظر - پلتفرم راهکارهای نرم‌افزاری و هوش مصنوعی", description: "با کمک هوش مصنوعی منتک، بهترین نرم‌افزارها و خدمات متناسب با کسب‌وکار شما پیشنهاد می‌شود.", canonical: "http://localhost:3000/", ogImage: null, noIndex: false } },
{ id: "p2", key: "blog", title: "بلاگ", seo: { title: "بلاگ — منتک", description: "یادداشت‌هایی درباره محصول، مهندسی و ساخت نرم‌افزار.", canonical: "http://localhost:3000/blog", ogImage: null, noIndex: false } },
{ id: "p3", key: "gallery", title: "گالری", seo: { title: "گالری — منتک", description: "تصاویر منتخب از پروژه‌ها و فضای استودیو منتک.", canonical: "http://localhost:3000/gallery", ogImage: null, noIndex: false } },
];
const now = new Date().toISOString();
let blog = [
{
id: "1",
id: "b1",
slug: "welcome-to-montech",
title: "به منتک خوش آمدید",
excerpt: "ما نرم‌افزاری می‌سازیم که واقعاً کار می‌کند.",
publishedAt: new Date().toISOString(),
excerpt: "ما نرم‌افزاری می‌سازیم که واقعاً کار می‌کند — و اینجا درباره‌اش می‌نویسیم.",
content: "منتک یک گروه توسعه نرم‌افزار است که پلتفرم وب، اپلیکیشن موبایل و زیرساخت ابری می‌سازد.\n\nدر این بلاگ درباره ساخت محصول، مهندسی و درس‌آموخته‌های استودیو می‌نویسیم.",
coverUrl: null,
published: true,
publishedAt: now,
seoTitle: "به منتک خوش آمدید — بلاگ",
seoDesc: "معرفی منتک و نحوه ساخت نرم‌افزار در تیم ما.",
ogImage: null,
canonical: null,
createdAt: now,
updatedAt: now,
},
];
let gallery = [];
let media = [];
let leads = [];
let seq = 1;
const id = (p) => `${p}${Date.now().toString(36)}${seq++}`;
const port = Number(process.env.PORT || 3050);
const server = http.createServer((req, res) => {
function readBody(req) {
return new Promise((resolve) => {
let raw = "";
req.on("data", (c) => (raw += c));
req.on("end", () => {
try {
resolve(raw ? JSON.parse(raw) : {});
} catch {
resolve({});
}
});
});
}
const server = http.createServer(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}
const url = req.url || "/";
const url = new URL(req.url || "/", "http://localhost");
const path = url.pathname;
const m = req.method;
const json = (data, code = 200) => {
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(data));
};
const seg = path.split("/").filter(Boolean);
if (url === "/api/health") return json({ ok: true, mock: true });
if (url === "/api/public/site") return json(site);
if (url === "/api/public/blog") return json(blog);
if (url.startsWith("/api/public/blog/")) {
const slug = decodeURIComponent(url.split("/").pop() || "");
const post = blog.find((p) => p.slug === slug);
return post
? json({ ...post, content: "محتوای نمونه برای پیش‌نمایش لوکال." })
: json({ message: "Not found" }, 404);
if (path === "/api/health") return json({ ok: true, mock: true });
// public
if (m === "GET" && path === "/api/public/site") return json({ settings, sections, pages });
if (m === "GET" && path === "/api/public/blog") return json(blog.filter((p) => p.published));
if (m === "GET" && path.startsWith("/api/public/blog/")) {
const post = blog.find((p) => p.slug === decodeURIComponent(seg[3] || "") && p.published);
return post ? json(post) : json({ message: "Not found" }, 404);
}
if (url === "/api/public/gallery") return json([]);
if (url.startsWith("/api/public/pages/")) {
const key = decodeURIComponent(url.split("/").pop() || "");
const page = site.pages.find((p) => p.key === key);
if (m === "GET" && path === "/api/public/gallery") return json(gallery.filter((g) => g.published !== false));
if (m === "GET" && path.startsWith("/api/public/pages/")) {
const page = pages.find((p) => p.key === decodeURIComponent(seg[3] || ""));
return page ? json(page) : json({ message: "Not found" }, 404);
}
if (m === "POST" && path === "/api/public/leads") {
const body = await readBody(req);
const phone = String(body.phone || "").replace(/[۰-۹]/g, (d) => String(d.charCodeAt(0) - 0x06f0)).replace(/[\s\-()]/g, "");
if (!body.name || String(body.name).trim().length < 2 || !/^\+?\d{7,15}$/.test(phone) || !body.businessType) {
return json({ message: ["validation failed"] }, 400);
}
const lead = { id: id("l"), name: String(body.name).trim(), phone, businessType: body.businessType, source: body.source || "consult", aiQuery: body.aiQuery || null, status: "new", note: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
leads.unshift(lead);
return json({ id: lead.id, createdAt: lead.createdAt }, 201);
}
json({ message: `Mock API — no route for ${url}` }, 404);
// auth
if (m === "POST" && path === "/api/auth/login") {
return json({ accessToken: "mock-token", admin: { id: "a1", mobile: "09121234567", name: "مدیر منتک" } });
}
if (path.startsWith("/api/admin") || path === "/api/auth/me") {
if (req.headers.authorization !== "Bearer mock-token") return json({ message: "Unauthorized" }, 401);
}
if (m === "GET" && path === "/api/auth/me") return json({ id: "a1", mobile: "09121234567", name: "مدیر منتک" });
// admin
if (path === "/api/admin/settings") {
if (m === "GET") return json(settings);
if (m === "PUT") {
settings = { ...settings, ...(await readBody(req)) };
sections.footer = { ...sections.footer, email: settings.email, phone: settings.phone, address: settings.address };
return json(settings);
}
}
if (m === "GET" && path === "/api/admin/sections") {
return json(SECTION_KEYS.map((key) => ({ key, data: sections[key], updatedAt: now, isDefault: false })));
}
if (seg[1] === "admin" && seg[2] === "sections" && seg[3]) {
const key = seg[3];
if (!SECTION_KEYS.includes(key)) return json({ message: "Unknown section" }, 400);
if (m === "GET") return json({ key, data: sections[key], updatedAt: now, isDefault: false, defaults: DEFAULT_SECTIONS[key] });
if (m === "PUT") {
const body = await readBody(req);
sections[key] = body.data;
return json({ key, data: sections[key] });
}
}
if (path === "/api/admin/leads" && m === "GET") {
const status = url.searchParams.get("status");
const page = Number(url.searchParams.get("page") || 1);
const pageSize = Number(url.searchParams.get("pageSize") || 20);
const filtered = status ? leads.filter((l) => l.status === status) : leads;
return json({ items: filtered.slice((page - 1) * pageSize, page * pageSize), total: filtered.length, page, pageSize, newCount: leads.filter((l) => l.status === "new").length });
}
if (seg[1] === "admin" && seg[2] === "leads" && seg[3]) {
const idx = leads.findIndex((l) => l.id === seg[3]);
if (idx < 0) return json({ message: "Not found" }, 404);
if (m === "PATCH") {
leads[idx] = { ...leads[idx], ...(await readBody(req)), updatedAt: new Date().toISOString() };
return json(leads[idx]);
}
if (m === "DELETE") return json(leads.splice(idx, 1)[0]);
}
if (path === "/api/admin/pages" && m === "GET") return json(pages);
if (seg[1] === "admin" && seg[2] === "pages" && seg[4] === "seo" && m === "PUT") {
const page = pages.find((p) => p.key === seg[3]);
if (!page) return json({ message: "Not found" }, 404);
page.seo = { ...page.seo, ...(await readBody(req)) };
return json(page.seo);
}
if (path === "/api/admin/blog") {
if (m === "GET") return json(blog);
if (m === "POST") {
const body = await readBody(req);
const post = { id: id("b"), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), publishedAt: body.published ? new Date().toISOString() : null, ...body };
blog.unshift(post);
return json(post, 201);
}
}
if (seg[1] === "admin" && seg[2] === "blog" && seg[3]) {
const idx = blog.findIndex((p) => p.id === seg[3]);
if (idx < 0) return json({ message: "Not found" }, 404);
if (m === "GET") return json(blog[idx]);
if (m === "PUT") {
blog[idx] = { ...blog[idx], ...(await readBody(req)), updatedAt: new Date().toISOString() };
return json(blog[idx]);
}
if (m === "DELETE") return json(blog.splice(idx, 1)[0]);
}
if (path === "/api/admin/gallery") {
if (m === "GET") return json(gallery);
if (m === "POST") {
const item = { id: id("g"), published: true, sortOrder: 0, ...(await readBody(req)) };
gallery.push(item);
return json(item, 201);
}
}
if (seg[1] === "admin" && seg[2] === "gallery" && seg[3]) {
const idx = gallery.findIndex((g) => g.id === seg[3]);
if (idx < 0) return json({ message: "Not found" }, 404);
if (m === "PUT") {
gallery[idx] = { ...gallery[idx], ...(await readBody(req)) };
return json(gallery[idx]);
}
if (m === "DELETE") return json(gallery.splice(idx, 1)[0]);
}
if (path === "/api/admin/media" && m === "GET") return json(media);
if (path === "/api/admin/media/upload" && m === "POST") {
req.resume();
const item = { id: id("m"), filename: "mock-upload.png", url: "/brand/logo.svg", mimeType: "image/svg+xml", size: 1024, createdAt: new Date().toISOString() };
media.unshift(item);
return json(item, 201);
}
if (seg[1] === "admin" && seg[2] === "media" && seg[3] && m === "DELETE") {
media = media.filter((x) => x.id !== seg[3]);
return json({ ok: true });
}
json({ message: `Mock API — no route for ${m} ${path}` }, 404);
});
server.listen(port, "127.0.0.1", () => {