import { cache } from "react"; import { and, asc, desc, eq } from "drizzle-orm"; import { db } from "@/lib/db"; import { category as categoryTable, portfolioAsset, portfolioProject, portfolioSection, } from "@/lib/db/schema"; import type { PortfolioProjectViewMode } from "@/lib/db/enums"; import { getPortfolioMediaBindings } from "@/lib/media"; import type { AppLocale } from "@/lib/locale"; type CategoryRecord = typeof categoryTable.$inferSelect; type SectionRecord = typeof portfolioSection.$inferSelect; type AssetRecord = typeof portfolioAsset.$inferSelect; type ProjectRecord = typeof portfolioProject.$inferSelect; export type LocalizedContent = { ar: string; en: string; de: string; }; export type PortfolioCategoryView = { id: string; slug: string; name: LocalizedContent; description: LocalizedContent; sortOrder: number; isActive: boolean; }; export type PortfolioSectionView = { id: string; type: SectionRecord["type"]; title: LocalizedContent; body: LocalizedContent; imagePath: string | null; mediaAssetId: string | null; linkUrl: string | null; sortOrder: number; }; export type PortfolioAssetView = { id: string; kind: AssetRecord["kind"]; filePath: string; mediaAssetId: string | null; alt: LocalizedContent; sortOrder: number; }; export type PortfolioProjectView = { id: string; slug: string; viewMode: PortfolioProjectViewMode; title: LocalizedContent; summary: LocalizedContent; clientName: string; projectYear: number; serviceLabel: LocalizedContent; previewUrl: string | null; coverImagePath: string | null; coverMediaAssetId: string | null; isFeatured: boolean; isPublished: boolean; publishedAt: Date | null; sortOrder: number; category: PortfolioCategoryView; sections: PortfolioSectionView[]; assets: PortfolioAssetView[]; }; export function resolvePortfolioProjectViewMode( value: string | null | undefined, ): PortfolioProjectViewMode { switch (value) { case "STORY": return "STORY"; case "CASE_STUDY": return "CASE_STUDY"; case "GRID": default: return "GRID"; } } function mapLocalizedContent(record: Record, prefix: string): LocalizedContent { return { ar: String(record[`${prefix}Ar`] ?? ""), en: String(record[`${prefix}En`] ?? ""), de: String(record[`${prefix}De`] ?? ""), }; } function mapCategory(record: CategoryRecord): PortfolioCategoryView { return { id: record.id, slug: record.slug, name: mapLocalizedContent(record, "name"), description: mapLocalizedContent(record, "description"), sortOrder: record.sortOrder, isActive: record.isActive, }; } function mapSection(record: SectionRecord, mediaAssetId: string | null): PortfolioSectionView { return { id: record.id, type: record.type, title: mapLocalizedContent(record, "title"), body: mapLocalizedContent(record, "body"), imagePath: record.imagePath, mediaAssetId, linkUrl: record.linkUrl, sortOrder: record.sortOrder, }; } function mapAsset(record: AssetRecord, mediaAssetId: string | null): PortfolioAssetView { return { id: record.id, kind: record.kind, filePath: record.filePath, mediaAssetId, alt: mapLocalizedContent(record, "alt"), sortOrder: record.sortOrder, }; } function mapProject( record: ProjectRecord & { category: CategoryRecord; sections: SectionRecord[]; assets: AssetRecord[]; }, mediaBindings?: { coverAssetId: string | null; sectionAssetIds: Record; assetIds: Record; }, ): PortfolioProjectView { return { id: record.id, slug: record.slug, viewMode: resolvePortfolioProjectViewMode(record.viewMode), title: mapLocalizedContent(record, "title"), summary: mapLocalizedContent(record, "summary"), clientName: record.clientName, projectYear: record.projectYear, serviceLabel: mapLocalizedContent(record, "serviceLabel"), previewUrl: record.previewUrl, coverImagePath: record.coverImagePath, coverMediaAssetId: mediaBindings?.coverAssetId ?? null, isFeatured: record.isFeatured, isPublished: record.isPublished, publishedAt: record.publishedAt, sortOrder: record.sortOrder, category: mapCategory(record.category), sections: record.sections.map((section) => mapSection(section, mediaBindings?.sectionAssetIds[section.id] ?? null), ), assets: record.assets.map((asset) => mapAsset(asset, mediaBindings?.assetIds[asset.id] ?? null)), }; } export function getLocalizedValue( content: LocalizedContent, locale: AppLocale, fallbackLocale: AppLocale = "de", ): string { const direct = content[locale]?.trim(); if (direct) { return direct; } const fallback = content[fallbackLocale]?.trim(); if (fallback) { return fallback; } return content.ar || content.en || content.de || ""; } export async function getAdminPortfolioCategories() { const categories = await db.query.category.findMany({ orderBy: [asc(categoryTable.sortOrder), asc(categoryTable.createdAt)], with: { projects: { columns: { id: true } } }, }); return categories.map((category) => ({ ...mapCategory(category), projectCount: category.projects.length, })); } export async function getActivePortfolioCategories() { const categories = await db.query.category.findMany({ where: eq(categoryTable.isActive, true), orderBy: [asc(categoryTable.sortOrder), asc(categoryTable.createdAt)], }); return categories.map(mapCategory); } export async function getActivePortfolioCategoryBySlug(slug: string) { const category = await db.query.category.findFirst({ where: and(eq(categoryTable.slug, slug), eq(categoryTable.isActive, true)), }); return category ? mapCategory(category) : null; } export async function getAdminPortfolioProjects(filters?: { categoryId?: string; status?: "all" | "draft" | "published"; }) { const conditions = [ ...(filters?.categoryId ? [eq(portfolioProject.categoryId, filters.categoryId)] : []), ...(filters?.status === "draft" ? [eq(portfolioProject.isPublished, false)] : filters?.status === "published" ? [eq(portfolioProject.isPublished, true)] : []), ]; const projects = await db.query.portfolioProject.findMany({ where: conditions.length ? and(...conditions) : undefined, with: { category: true, sections: { orderBy: [asc(portfolioSection.sortOrder), asc(portfolioSection.createdAt)] }, assets: { orderBy: [asc(portfolioAsset.sortOrder), asc(portfolioAsset.createdAt)] }, }, orderBy: [asc(portfolioProject.sortOrder), desc(portfolioProject.createdAt)], }); return projects.map((project) => mapProject(project)); } export async function getPublishedPortfolioProjects(filters?: { categorySlug?: string }) { const projects = await db.query.portfolioProject.findMany({ where: eq(portfolioProject.isPublished, true), with: { category: true, sections: { orderBy: [asc(portfolioSection.sortOrder), asc(portfolioSection.createdAt)] }, assets: { orderBy: [asc(portfolioAsset.sortOrder), asc(portfolioAsset.createdAt)] }, }, orderBy: [ asc(portfolioProject.sortOrder), desc(portfolioProject.publishedAt), desc(portfolioProject.createdAt), ], }); // Prisma filtered on the related category (active + optional slug); the // relational query filters the main table only, so narrow here. return projects .filter( (project) => project.category.isActive && (!filters?.categorySlug || project.category.slug === filters.categorySlug), ) .map((project) => mapProject(project)); } export const getPublishedPortfolioProjectBySlug = cache(async function (slug: string) { const project = await db.query.portfolioProject.findFirst({ where: and(eq(portfolioProject.slug, slug), eq(portfolioProject.isPublished, true)), with: { category: true, sections: { orderBy: [asc(portfolioSection.sortOrder), asc(portfolioSection.createdAt)] }, assets: { orderBy: [asc(portfolioAsset.sortOrder), asc(portfolioAsset.createdAt)] }, }, }); if (!project || !project.category.isActive) { return null; } return mapProject(project); }); export async function getAdminPortfolioProjectById(id: string) { const project = await db.query.portfolioProject.findFirst({ where: eq(portfolioProject.id, id), with: { category: true, sections: { orderBy: [asc(portfolioSection.sortOrder), asc(portfolioSection.createdAt)] }, assets: { orderBy: [asc(portfolioAsset.sortOrder), asc(portfolioAsset.createdAt)] }, }, }); if (!project) { return null; } const mediaBindings = await getPortfolioMediaBindings(project.id); return mapProject(project, mediaBindings); }