368 lines
8.5 KiB
TypeScript
368 lines
8.5 KiB
TypeScript
import type {
|
|
Category,
|
|
PortfolioAsset,
|
|
PortfolioProject,
|
|
PortfolioProjectViewMode,
|
|
PortfolioSection,
|
|
} from "@prisma/client";
|
|
|
|
import { getPortfolioMediaBindings } from "@/lib/media";
|
|
import type { AppLocale } from "@/lib/locale";
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
type CategoryRecord = Pick<
|
|
Category,
|
|
| "id"
|
|
| "slug"
|
|
| "nameAr"
|
|
| "nameEn"
|
|
| "nameDe"
|
|
| "descriptionAr"
|
|
| "descriptionEn"
|
|
| "descriptionDe"
|
|
| "sortOrder"
|
|
| "isActive"
|
|
>;
|
|
|
|
type SectionRecord = Pick<
|
|
PortfolioSection,
|
|
| "id"
|
|
| "type"
|
|
| "titleAr"
|
|
| "titleEn"
|
|
| "titleDe"
|
|
| "bodyAr"
|
|
| "bodyEn"
|
|
| "bodyDe"
|
|
| "imagePath"
|
|
| "linkUrl"
|
|
| "sortOrder"
|
|
>;
|
|
|
|
type AssetRecord = Pick<
|
|
PortfolioAsset,
|
|
"id" | "kind" | "filePath" | "altAr" | "altEn" | "altDe" | "sortOrder"
|
|
>;
|
|
|
|
type ProjectRecord = Pick<
|
|
PortfolioProject,
|
|
| "id"
|
|
| "slug"
|
|
| "viewMode"
|
|
| "titleAr"
|
|
| "titleEn"
|
|
| "titleDe"
|
|
| "summaryAr"
|
|
| "summaryEn"
|
|
| "summaryDe"
|
|
| "clientName"
|
|
| "projectYear"
|
|
| "serviceLabelAr"
|
|
| "serviceLabelEn"
|
|
| "serviceLabelDe"
|
|
| "previewUrl"
|
|
| "coverImagePath"
|
|
| "isFeatured"
|
|
| "isPublished"
|
|
| "publishedAt"
|
|
| "sortOrder"
|
|
>;
|
|
|
|
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<string, unknown>, 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<string, string>;
|
|
assetIds: Record<string, string>;
|
|
},
|
|
): 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 prisma.category.findMany({
|
|
include: {
|
|
_count: {
|
|
select: {
|
|
projects: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
|
});
|
|
|
|
return categories.map((category) => ({
|
|
...mapCategory(category),
|
|
projectCount: category._count.projects,
|
|
}));
|
|
}
|
|
|
|
export async function getActivePortfolioCategories() {
|
|
const categories = await prisma.category.findMany({
|
|
where: {
|
|
isActive: true,
|
|
},
|
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
|
});
|
|
|
|
return categories.map(mapCategory);
|
|
}
|
|
|
|
export async function getAdminPortfolioProjects(filters?: {
|
|
categoryId?: string;
|
|
status?: "all" | "draft" | "published";
|
|
}) {
|
|
const projects = await prisma.portfolioProject.findMany({
|
|
where: {
|
|
...(filters?.categoryId ? { categoryId: filters.categoryId } : {}),
|
|
...(filters?.status === "draft"
|
|
? { isPublished: false }
|
|
: filters?.status === "published"
|
|
? { isPublished: true }
|
|
: {}),
|
|
},
|
|
include: {
|
|
category: true,
|
|
sections: {
|
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
|
},
|
|
assets: {
|
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
|
},
|
|
},
|
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }],
|
|
});
|
|
|
|
return projects.map((project) => mapProject(project));
|
|
}
|
|
|
|
export async function getPublishedPortfolioProjects(filters?: { categorySlug?: string }) {
|
|
const projects = await prisma.portfolioProject.findMany({
|
|
where: {
|
|
isPublished: true,
|
|
category: {
|
|
isActive: true,
|
|
...(filters?.categorySlug ? { slug: filters.categorySlug } : {}),
|
|
},
|
|
},
|
|
include: {
|
|
category: true,
|
|
sections: {
|
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
|
},
|
|
assets: {
|
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
|
},
|
|
},
|
|
orderBy: [{ sortOrder: "asc" }, { publishedAt: "desc" }, { createdAt: "desc" }],
|
|
});
|
|
|
|
return projects.map((project) => mapProject(project));
|
|
}
|
|
|
|
export async function getPublishedPortfolioProjectBySlug(slug: string) {
|
|
const project = await prisma.portfolioProject.findFirst({
|
|
where: {
|
|
slug,
|
|
isPublished: true,
|
|
category: {
|
|
isActive: true,
|
|
},
|
|
},
|
|
include: {
|
|
category: true,
|
|
sections: {
|
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
|
},
|
|
assets: {
|
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
return project ? mapProject(project) : null;
|
|
}
|
|
|
|
export async function getAdminPortfolioProjectById(id: string) {
|
|
const project = await prisma.portfolioProject.findUnique({
|
|
where: {
|
|
id,
|
|
},
|
|
include: {
|
|
category: true,
|
|
sections: {
|
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
|
},
|
|
assets: {
|
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!project) {
|
|
return null;
|
|
}
|
|
|
|
const mediaBindings = await getPortfolioMediaBindings(project.id);
|
|
|
|
return mapProject(project, mediaBindings);
|
|
}
|