Initial Montech monorepo: Nuxt landing, Vue admin, Nest API.

Scaffold SSR site with CMS/blog/gallery/SEO admin and deploy configs for montech.ir.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-30 18:42:28 +03:30
co-authored by Cursor
commit 7e8e1b1f1b
96 changed files with 19336 additions and 0 deletions
@@ -0,0 +1,113 @@
-- CreateSchema
CREATE SCHEMA IF NOT EXISTS "public";
-- CreateTable
CREATE TABLE "Admin" (
"id" TEXT NOT NULL,
"mobile" TEXT NOT NULL,
"email" TEXT,
"passwordHash" TEXT NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Admin_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "SiteSettings" (
"id" TEXT NOT NULL DEFAULT 'default',
"brandName" TEXT NOT NULL DEFAULT 'Montech',
"tagline" TEXT NOT NULL DEFAULT 'Software Development Group',
"email" TEXT NOT NULL DEFAULT 'hello@montech.ir',
"phone" TEXT NOT NULL DEFAULT '+98 21 0000 0000',
"address" TEXT NOT NULL DEFAULT 'Tehran, IR',
"timezone" TEXT NOT NULL DEFAULT 'Asia/Tehran',
"socials" JSONB NOT NULL DEFAULT '[]',
"logoUrl" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SiteSettings_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "Page" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"title" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Page_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "PageSeo" (
"id" TEXT NOT NULL,
"pageId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"ogImage" TEXT,
"canonical" TEXT,
"noIndex" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "PageSeo_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "SectionContent" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"data" JSONB NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SectionContent_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "BlogPost" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"title" TEXT NOT NULL,
"excerpt" TEXT NOT NULL DEFAULT '',
"content" TEXT NOT NULL DEFAULT '',
"coverUrl" TEXT,
"published" BOOLEAN NOT NULL DEFAULT false,
"publishedAt" TIMESTAMP(3),
"seoTitle" TEXT,
"seoDesc" TEXT,
"ogImage" TEXT,
"canonical" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BlogPost_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "GalleryItem" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"imageUrl" TEXT NOT NULL,
"category" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"published" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "GalleryItem_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "Media" (
"id" TEXT NOT NULL,
"filename" TEXT NOT NULL,
"url" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Media_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "Admin_mobile_key" ON "Admin"("mobile");
CREATE UNIQUE INDEX "Admin_email_key" ON "Admin"("email");
CREATE UNIQUE INDEX "Page_key_key" ON "Page"("key");
CREATE UNIQUE INDEX "PageSeo_pageId_key" ON "PageSeo"("pageId");
CREATE UNIQUE INDEX "SectionContent_key_key" ON "SectionContent"("key");
CREATE UNIQUE INDEX "BlogPost_slug_key" ON "BlogPost"("slug");
ALTER TABLE "PageSeo" ADD CONSTRAINT "PageSeo_pageId_fkey" FOREIGN KEY ("pageId") REFERENCES "Page"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
# This file tells Prisma which database provider to use for migrations.
provider = "postgresql"
+95
View File
@@ -0,0 +1,95 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Admin {
id String @id @default(cuid())
mobile String @unique
email String? @unique
passwordHash String
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model SiteSettings {
id String @id @default("default")
brandName String @default("Montech")
tagline String @default("Software Development Group")
email String @default("hello@montech.ir")
phone String @default("+98 21 0000 0000")
address String @default("Tehran, IR")
timezone String @default("Asia/Tehran")
socials Json @default("[]")
logoUrl String?
updatedAt DateTime @updatedAt
}
model Page {
id String @id @default(cuid())
key String @unique
title String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
seo PageSeo?
}
model PageSeo {
id String @id @default(cuid())
pageId String @unique
page Page @relation(fields: [pageId], references: [id], onDelete: Cascade)
title String
description String @default("")
ogImage String?
canonical String?
noIndex Boolean @default(false)
}
model SectionContent {
id String @id @default(cuid())
key String @unique
data Json
updatedAt DateTime @updatedAt
}
model BlogPost {
id String @id @default(cuid())
slug String @unique
title String
excerpt String @default("")
content String @default("")
coverUrl String?
published Boolean @default(false)
publishedAt DateTime?
seoTitle String?
seoDesc String?
ogImage String?
canonical String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model GalleryItem {
id String @id @default(cuid())
title String
imageUrl String
category String?
sortOrder Int @default(0)
published Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Media {
id String @id @default(cuid())
filename String
url String
mimeType String
size Int
createdAt DateTime @default(now())
}
+234
View File
@@ -0,0 +1,234 @@
import { PrismaClient } from "@prisma/client";
import * as bcrypt from "bcryptjs";
const prisma = new PrismaClient();
const defaultSections: Record<string, unknown> = {
hero: {
eyebrow: "SOFTWARE DEVELOPMENT GROUP",
location: "[ EST. 2020 — TEHRAN / REMOTE ]",
lines: ["We craft", "software that", "performs"],
highlightWord: "software",
body: "We are a senior engineering group shipping web platforms, mobile apps and cloud infrastructure for companies that refuse to blend in.",
cta: "SEE OUR WORK",
badge: "MONTECH ENGINEERING • SINCE 2020 •",
coords: "35.6892° N / 51.3890° E",
projectsLabel: "120+ PROJECTS SHIPPED",
},
marquee: {
items: [
"WEB PLATFORMS",
"MOBILE APPS",
"CLOUD & DEVOPS",
"AI SOLUTIONS",
"DESIGN SYSTEMS",
"API ARCHITECTURE",
],
},
manifesto: {
tag: "Who we are",
words: [
{ text: "We", outline: false },
{ text: "build", outline: false },
{ text: "software", outline: true },
{ text: "that", outline: false },
{ text: "moves", outline: false },
{ text: "the", outline: false },
{ text: "needle.", outline: false },
{ text: "MONTECH", outline: false, volt: true },
],
body: "Strategy, design and engineering under one roof — shipped with obsessive attention to performance and craft.",
},
services: {
tag: "What we do",
items: [
{
n: "01",
title: "Product Strategy",
desc: "We turn fuzzy ideas into validated roadmaps. Discovery sprints, user research and rapid prototyping de-risk your build before a single line of code is written.",
tags: ["Discovery Sprints", "User Research", "Roadmapping", "MVP Scoping"],
},
{
n: "02",
title: "Web Engineering",
desc: "Blazing-fast, scalable web platforms with modern stacks. From marketing sites to complex SaaS dashboards — engineered for performance, accessibility and conversion.",
tags: ["React / Next.js", "Nuxt", "Node.js", "NestJS"],
},
{
n: "03",
title: "Mobile Applications",
desc: "Native-quality iOS and Android experiences from a single codebase — or fully native when it matters.",
tags: ["React Native", "Flutter", "Swift", "Kotlin"],
},
{
n: "04",
title: "Cloud & DevOps",
desc: "Infrastructure that scales with your ambition. Resilient cloud systems, automated deployments, secure and observable platforms.",
tags: ["AWS / GCP", "Kubernetes", "Terraform", "CI/CD"],
},
{
n: "05",
title: "AI & Data",
desc: "Practical AI, not science projects. LLM integrations, intelligent automation and data pipelines.",
tags: ["LLM Integration", "ML Pipelines", "Data Engineering", "Analytics"],
},
],
},
work: {
tag: "Selected work",
items: [
{ n: "01", title: "PULSEBOARD", cat: "Fintech Analytics Platform", year: "2024" },
{ n: "02", title: "AEROLOOP", cat: "Logistics SaaS", year: "2024" },
{ n: "03", title: "MEDSYNC", cat: "HealthTech Platform", year: "2023" },
{ n: "04", title: "NOVAPAY", cat: "Payment Infrastructure", year: "2023" },
],
},
process: {
tag: "How we ship",
steps: [
{ n: "01", title: "DISCOVER", desc: "Workshops, research and alignment on outcomes that matter." },
{ n: "02", title: "DESIGN", desc: "Systems, flows and interfaces that feel inevitable." },
{ n: "03", title: "DEVELOP", desc: "Senior engineers shipping production-ready software." },
{ n: "04", title: "DEPLOY & SCALE", desc: "CI/CD, observability and infrastructure that grows with you." },
],
},
stats: {
items: [
{ value: 120, suffix: "+", label: "Projects shipped" },
{ value: 40, suffix: "+", label: "Clients worldwide" },
{ value: 15, suffix: "", label: "Engineers & designers" },
{ value: 98, suffix: "%", label: "Client retention" },
],
},
team: {
tag: "The collective",
headline: "Meet the collective",
body: "A tight crew of senior builders — product, design and engineering.",
members: [
{ name: "Alexandra Reyes", role: "CEO & CO-FOUNDER" },
{ name: "Marcus Chen", role: "CTO & CO-FOUNDER" },
{ name: "Sofia Laurent", role: "HEAD OF DESIGN" },
{ name: "David Okafor", role: "PRINCIPAL ENGINEER" },
],
},
footer: {
tag: "Contact",
marquee: ["GOT A PROJECT IN MIND", "SAY HELLO", "LET'S WORK TOGETHER"],
cta: "Start a project",
email: "hello@montech.ir",
phone: "+98 21 0000 0000",
address: ["Tehran", "Iran"],
socials: ["LinkedIn", "GitHub", "Dribbble"],
},
};
async function main() {
const mobile = process.env.ADMIN_MOBILE || "09121234567";
const password = process.env.ADMIN_PASSWORD || "change-me-strong";
const name = process.env.ADMIN_NAME || "Montech Admin";
const hash = await bcrypt.hash(password, 10);
await prisma.admin.upsert({
where: { mobile },
create: { mobile, passwordHash: hash, name, email: "admin@montech.ir" },
update: { passwordHash: hash, name },
});
await prisma.siteSettings.upsert({
where: { id: "default" },
create: {
id: "default",
brandName: "Montech",
tagline: "Software Development Group",
email: "hello@montech.ir",
phone: "+98 21 0000 0000",
address: "Tehran, IR",
timezone: "Asia/Tehran",
socials: [
{ label: "LinkedIn", url: "https://linkedin.com" },
{ label: "GitHub", url: "https://github.com" },
],
},
update: {},
});
const pages = [
{
key: "home",
title: "Home",
seo: {
title: "Montech — Software Development Group",
description:
"Montech engineers high-performance web platforms, mobile apps and cloud infrastructure. Based in Tehran, shipping worldwide.",
canonical: "https://montech.ir/",
},
},
{
key: "blog",
title: "Blog",
seo: {
title: "Blog — Montech",
description: "Notes on product, engineering and shipping software.",
canonical: "https://montech.ir/blog",
},
},
{
key: "gallery",
title: "Gallery",
seo: {
title: "Gallery — Montech",
description: "Selected visuals from Montech projects and studio life.",
canonical: "https://montech.ir/gallery",
},
},
];
for (const p of pages) {
const page = await prisma.page.upsert({
where: { key: p.key },
create: { key: p.key, title: p.title },
update: { title: p.title },
});
await prisma.pageSeo.upsert({
where: { pageId: page.id },
create: { pageId: page.id, ...p.seo },
update: { ...p.seo },
});
}
for (const [key, data] of Object.entries(defaultSections)) {
await prisma.sectionContent.upsert({
where: { key },
create: { key, data: data as object },
update: { data: data as object },
});
}
const existingPost = await prisma.blogPost.findUnique({ where: { slug: "welcome-to-montech" } });
if (!existingPost) {
await prisma.blogPost.create({
data: {
slug: "welcome-to-montech",
title: "Welcome to Montech",
excerpt: "We build software that performs — and this is where we write about it.",
content:
"<p>Montech is a software development group shipping web platforms, mobile apps and cloud infrastructure.</p><p>This blog covers product craft, engineering practice and lessons from the studio.</p>",
published: true,
publishedAt: new Date(),
seoTitle: "Welcome to Montech — Blog",
seoDesc: "Introducing Montech and how we ship software.",
},
});
}
console.log("Seed complete. Admin:", mobile);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});