// src/lib/assetFolders.ts // ───────────────────────────────────────────────────────────────────────────── // Server-side helpers that ensure every asset bucket exists when an editor // creates a new entity in HQ Command. Without this, the editor's first // upload error-paths because the target subfolder doesn't exist yet. // // Each scope has a known set of "buckets" — the well-known subfolder layout // the front-end expects (e.g. cases use videos/, renders/, gallery/). We // pre-create them all so editors can drop files anywhere without needing // to think about server-side folder structure. // ───────────────────────────────────────────────────────────────────────────── import "server-only"; import fs from "fs"; import path from "path"; export type AssetScope = "cases" | "applications" | "news" | "parts"; const SCOPE_ROOT: Record = { cases: path.join(process.cwd(), "public", "cases"), applications: path.join(process.cwd(), "public", "applications"), news: path.join(process.cwd(), "public", "news"), parts: path.join(process.cwd(), "public", "parts"), }; // Buckets per scope — the subfolders the front-end reads from. // Adding a new bucket here is the only place in the codebase you need to // touch to introduce a new asset type. const SCOPE_BUCKETS: Record = { cases: ["videos", "renders", "gallery", "datasheet", "models"], applications: ["videos", "renders", "gallery", "datasheet"], news: ["gallery"], parts: ["renders", "gallery"], }; /** * Create the slug folder + every bucket subfolder if they don't exist. * Idempotent — safe to call on every entity create or edit. */ export function ensureAssetFolders(scope: AssetScope, slug: string): void { if (!slug) return; try { const safeSlug = slug.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, ""); if (!safeSlug) return; const root = SCOPE_ROOT[scope]; const baseDir = path.join(root, safeSlug); fs.mkdirSync(baseDir, { recursive: true }); for (const bucket of SCOPE_BUCKETS[scope]) { fs.mkdirSync(path.join(baseDir, bucket), { recursive: true }); } } catch (error) { console.error(`[assetFolders] Failed to ensure ${scope}/${slug}:`, error); } } /** * Convert a free-text title into the same slug the front-end uses. * Mirrors `nodeToSlug` in ApplicationClient.tsx so the folder name matches * the path the page renders for that node's assets. */ export function titleToSlug(title: string): string { return title .toLowerCase() .trim() .replace(/[^\w\s-]/g, "") .replace(/[\s_-]+/g, "-") .replace(/^-+|-+$/g, ""); }