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
+58
View File
@@ -0,0 +1,58 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
UseGuards,
} from "@nestjs/common";
import { BlogService } from "./blog.service";
import { JwtAuthGuard } from "../auth/jwt-auth.guard";
import { CreateBlogDto, UpdateBlogDto } from "./blog.dto";
@Controller()
export class BlogController {
constructor(private blog: BlogService) {}
@Get("public/blog")
listPublic() {
return this.blog.listPublic();
}
@Get("public/blog/:slug")
getPublic(@Param("slug") slug: string) {
return this.blog.getBySlug(slug);
}
@UseGuards(JwtAuthGuard)
@Get("admin/blog")
listAdmin() {
return this.blog.listAdmin();
}
@UseGuards(JwtAuthGuard)
@Get("admin/blog/:id")
getAdmin(@Param("id") id: string) {
return this.blog.getAdmin(id);
}
@UseGuards(JwtAuthGuard)
@Post("admin/blog")
create(@Body() dto: CreateBlogDto) {
return this.blog.create(dto);
}
@UseGuards(JwtAuthGuard)
@Put("admin/blog/:id")
update(@Param("id") id: string, @Body() dto: UpdateBlogDto) {
return this.blog.update(id, dto);
}
@UseGuards(JwtAuthGuard)
@Delete("admin/blog/:id")
remove(@Param("id") id: string) {
return this.blog.remove(id);
}
}
+32
View File
@@ -0,0 +1,32 @@
import {
IsBoolean,
IsOptional,
IsString,
MinLength,
} from "class-validator";
export class CreateBlogDto {
@IsString() @MinLength(1) slug!: string;
@IsString() @MinLength(1) title!: string;
@IsOptional() @IsString() excerpt?: string;
@IsOptional() @IsString() content?: string;
@IsOptional() @IsString() coverUrl?: string;
@IsOptional() @IsBoolean() published?: boolean;
@IsOptional() @IsString() seoTitle?: string;
@IsOptional() @IsString() seoDesc?: string;
@IsOptional() @IsString() ogImage?: string;
@IsOptional() @IsString() canonical?: string;
}
export class UpdateBlogDto {
@IsOptional() @IsString() @MinLength(1) slug?: string;
@IsOptional() @IsString() @MinLength(1) title?: string;
@IsOptional() @IsString() excerpt?: string;
@IsOptional() @IsString() content?: string;
@IsOptional() @IsString() coverUrl?: string;
@IsOptional() @IsBoolean() published?: boolean;
@IsOptional() @IsString() seoTitle?: string;
@IsOptional() @IsString() seoDesc?: string;
@IsOptional() @IsString() ogImage?: string;
@IsOptional() @IsString() canonical?: string;
}
+9
View File
@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
import { BlogService } from "./blog.service";
import { BlogController } from "./blog.controller";
@Module({
controllers: [BlogController],
providers: [BlogService],
})
export class BlogModule {}
+92
View File
@@ -0,0 +1,92 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
@Injectable()
export class BlogService {
constructor(private prisma: PrismaService) {}
listPublic() {
return this.prisma.blogPost.findMany({
where: { published: true },
orderBy: { publishedAt: "desc" },
select: {
id: true,
slug: true,
title: true,
excerpt: true,
coverUrl: true,
publishedAt: true,
seoTitle: true,
seoDesc: true,
},
});
}
async getBySlug(slug: string) {
const post = await this.prisma.blogPost.findFirst({
where: { slug, published: true },
});
if (!post) throw new NotFoundException();
return post;
}
listAdmin() {
return this.prisma.blogPost.findMany({ orderBy: { updatedAt: "desc" } });
}
getAdmin(id: string) {
return this.prisma.blogPost.findUniqueOrThrow({ where: { id } });
}
create(data: {
slug: string;
title: string;
excerpt?: string;
content?: string;
coverUrl?: string;
published?: boolean;
seoTitle?: string;
seoDesc?: string;
ogImage?: string;
canonical?: string;
}) {
return this.prisma.blogPost.create({
data: {
...data,
publishedAt: data.published ? new Date() : null,
},
});
}
async update(
id: string,
data: {
slug?: string;
title?: string;
excerpt?: string;
content?: string;
coverUrl?: string | null;
published?: boolean;
seoTitle?: string | null;
seoDesc?: string | null;
ogImage?: string | null;
canonical?: string | null;
},
) {
const existing = await this.prisma.blogPost.findUniqueOrThrow({ where: { id } });
const publishedAt =
data.published === true && !existing.publishedAt
? new Date()
: data.published === false
? null
: existing.publishedAt;
return this.prisma.blogPost.update({
where: { id },
data: { ...data, publishedAt },
});
}
remove(id: string) {
return this.prisma.blogPost.delete({ where: { id } });
}
}