Files
MahdiandCursor b0088c35d1 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>
2026-10-01 20:36:06 +03:30

229 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 require = createRequire(import.meta.url);
const { DEFAULT_SECTIONS, SECTION_KEYS } = require("../packages/shared/dist/index.js");
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 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: "b1",
slug: "welcome-to-montech",
title: "به منتک خوش آمدید",
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);
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,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 = 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 (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 (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);
}
// 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", () => {
console.log(`[mock-api] http://127.0.0.1:${port}`);
});