mirror of
https://git.meshkee.com/novintrades/website.git
synced 2026-08-11 20:50:58 +04:30
179 lines
5.8 KiB
TypeScript
179 lines
5.8 KiB
TypeScript
import type { FastifyPluginAsync } from "fastify";
|
|
import { VerificationStatus } from "@prisma/client";
|
|
import { z } from "zod";
|
|
import { slugify } from "../lib/slug.js";
|
|
|
|
const contactSchema = z.object({
|
|
type: z.enum([
|
|
"phone",
|
|
"landline",
|
|
"email",
|
|
"instagram",
|
|
"website",
|
|
"whatsapp",
|
|
"other",
|
|
]),
|
|
value: z.string(),
|
|
});
|
|
|
|
const createSchema = z.object({
|
|
title: z.string().min(1),
|
|
slug: z.string().min(1).optional(),
|
|
abstract: z.string().optional(),
|
|
content: z.string().optional(),
|
|
imageUrl: z.string().url().nullable().optional(),
|
|
galleryUrls: z.array(z.string().url()).optional(),
|
|
tags: z.array(z.string()).optional(),
|
|
country: z.string().min(1),
|
|
city: z.string().optional(),
|
|
address: z.string().optional(),
|
|
contacts: z.array(contactSchema).optional(),
|
|
categoryIds: z.array(z.string().cuid()).optional(),
|
|
authorId: z.string().cuid(),
|
|
verificationStatus: z.nativeEnum(VerificationStatus).optional(),
|
|
publishedAt: z.coerce.date().nullable().optional(),
|
|
});
|
|
|
|
function assertNoContentImages(content: string | undefined, reply: {
|
|
status: (code: number) => { send: (body: unknown) => unknown };
|
|
}) {
|
|
if (content && /<img\b/i.test(content)) {
|
|
return reply.status(400).send({
|
|
error: "Brand main text cannot include images. Use the gallery instead.",
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export const brandRoutes: FastifyPluginAsync = async (app) => {
|
|
app.get("/", async () => {
|
|
return app.prisma.brand.findMany({
|
|
orderBy: { createdAt: "desc" },
|
|
include: {
|
|
author: { select: { id: true, email: true, name: true } },
|
|
categories: { include: { category: true } },
|
|
},
|
|
});
|
|
});
|
|
|
|
app.get("/:id", async (request, reply) => {
|
|
const { id } = request.params as { id: string };
|
|
const brand = await app.prisma.brand.findUnique({
|
|
where: { id },
|
|
include: {
|
|
author: { select: { id: true, email: true, name: true } },
|
|
categories: { include: { category: true } },
|
|
},
|
|
});
|
|
|
|
if (!brand) {
|
|
return reply.status(404).send({ error: "Brand not found" });
|
|
}
|
|
|
|
return brand;
|
|
});
|
|
|
|
app.post("/", async (request, reply) => {
|
|
const parsed = createSchema.safeParse(request.body);
|
|
if (!parsed.success) {
|
|
return reply.status(400).send({ error: parsed.error.flatten() });
|
|
}
|
|
|
|
const contentBlocked = assertNoContentImages(parsed.data.content, reply);
|
|
if (contentBlocked) return contentBlocked;
|
|
|
|
const slug = slugify(parsed.data.slug ?? parsed.data.title);
|
|
const categoryIds = parsed.data.categoryIds ?? [];
|
|
const galleryUrls = parsed.data.galleryUrls ?? [];
|
|
|
|
const brand = await app.prisma.brand.create({
|
|
data: {
|
|
title: parsed.data.title,
|
|
slug,
|
|
abstract: parsed.data.abstract,
|
|
content: parsed.data.content ?? "",
|
|
imageUrl: parsed.data.imageUrl ?? null,
|
|
galleryUrls,
|
|
tags: parsed.data.tags ?? [],
|
|
country: parsed.data.country,
|
|
city: parsed.data.city,
|
|
address: parsed.data.address,
|
|
contacts: parsed.data.contacts ?? [],
|
|
authorId: parsed.data.authorId,
|
|
verificationStatus:
|
|
parsed.data.verificationStatus ?? VerificationStatus.PENDING,
|
|
publishedAt: parsed.data.publishedAt ?? null,
|
|
categories: {
|
|
create: categoryIds.map((categoryId) => ({ categoryId })),
|
|
},
|
|
},
|
|
include: {
|
|
author: { select: { id: true, email: true, name: true } },
|
|
categories: { include: { category: true } },
|
|
},
|
|
});
|
|
|
|
return reply.status(201).send(brand);
|
|
});
|
|
|
|
app.patch("/:id", async (request, reply) => {
|
|
const { id } = request.params as { id: string };
|
|
const updateSchema = createSchema.partial().omit({ authorId: true });
|
|
const parsed = updateSchema.safeParse(request.body);
|
|
if (!parsed.success) {
|
|
return reply.status(400).send({ error: parsed.error.flatten() });
|
|
}
|
|
|
|
const contentBlocked = assertNoContentImages(parsed.data.content, reply);
|
|
if (contentBlocked) return contentBlocked;
|
|
|
|
const existing = await app.prisma.brand.findUnique({ where: { id } });
|
|
if (!existing) {
|
|
return reply.status(404).send({ error: "Brand not found" });
|
|
}
|
|
|
|
const data = parsed.data;
|
|
const categoryIds = data.categoryIds;
|
|
const galleryUrls = data.galleryUrls;
|
|
|
|
const brand = await app.prisma.brand.update({
|
|
where: { id },
|
|
data: {
|
|
...(data.title !== undefined ? { title: data.title } : {}),
|
|
...(data.title !== undefined || data.slug !== undefined
|
|
? { slug: slugify(data.slug ?? data.title ?? existing.title) }
|
|
: {}),
|
|
...(data.abstract !== undefined ? { abstract: data.abstract } : {}),
|
|
...(data.content !== undefined ? { content: data.content } : {}),
|
|
...(data.imageUrl !== undefined ? { imageUrl: data.imageUrl } : {}),
|
|
...(galleryUrls !== undefined ? { galleryUrls } : {}),
|
|
...(data.tags !== undefined ? { tags: data.tags } : {}),
|
|
...(data.country !== undefined ? { country: data.country } : {}),
|
|
...(data.city !== undefined ? { city: data.city } : {}),
|
|
...(data.address !== undefined ? { address: data.address } : {}),
|
|
...(data.contacts !== undefined ? { contacts: data.contacts } : {}),
|
|
...(data.verificationStatus !== undefined
|
|
? { verificationStatus: data.verificationStatus }
|
|
: {}),
|
|
...(data.publishedAt !== undefined
|
|
? { publishedAt: data.publishedAt }
|
|
: {}),
|
|
...(categoryIds
|
|
? {
|
|
categories: {
|
|
deleteMany: {},
|
|
create: categoryIds.map((categoryId) => ({ categoryId })),
|
|
},
|
|
}
|
|
: {}),
|
|
},
|
|
include: {
|
|
author: { select: { id: true, email: true, name: true } },
|
|
categories: { include: { category: true } },
|
|
},
|
|
});
|
|
|
|
return brand;
|
|
});
|
|
};
|