feat: full site build — Project/Melody schema (Option A), admin CRUD, public sections, uploads, email+SMTP, internal analytics, legal pages, docs

This commit is contained in:
2026-08-05 20:53:40 +02:00
parent d87f3033c6
commit c26f41511f
122 changed files with 15068 additions and 41 deletions
+29
View File
@@ -0,0 +1,29 @@
import { z } from "zod";
export const melodyStatusSchema = z.enum(["DRAFT", "PUBLISHED", "ARCHIVED"]);
const audioPath = z.string().trim().regex(/^\/api\/uploads\/audios\/[^/]+$/, "Use an uploaded audio path.");
const imagePath = z.string().trim().regex(/^\/api\/uploads\/images\/[^/]+$/, "Use an uploaded image path.");
export const melodySchema = z.object({
slug: z
.string()
.trim()
.min(1, "Slug is required.")
.max(100, "Slug must be 100 characters or fewer.")
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use lowercase letters, numbers, and hyphens only."),
titleAr: z.string().trim().min(1, "Arabic title is required.").max(160),
titleEn: z.string().trim().min(1, "English title is required.").max(160),
descAr: z.string().trim().max(4000).optional(),
descEn: z.string().trim().max(4000).optional(),
audioFile: audioPath,
coverImage: z.union([z.literal(""), imagePath]).optional(),
durationSec: z.number().int().min(0).max(86400).nullable(),
isDownloadable: z.boolean(),
status: melodyStatusSchema,
isFeatured: z.boolean(),
sortOrder: z.number().int().min(0).max(9999),
categoryId: z.string().cuid("Choose a valid melody category."),
});
export const melodyIdSchema = z.string().cuid("Invalid melody id.");
export type MelodyInput = z.infer<typeof melodySchema>;