content: add approved public portfolio projection

This commit is contained in:
2026-09-02 01:00:36 +03:00
parent 6428b5dcd6
commit 49a4f75c8d
11 changed files with 460 additions and 11 deletions

View File

@@ -9,6 +9,11 @@ const socialHosts = {
GitLab: 'gitlab.com',
LinkedIn: 'linkedin.com',
} as const;
const approvedPrivateProjectTitles = [
'Private self-hosted medication tracker',
'Private healthcare platform',
'Private game prototype',
] as const;
const hasUnpairedSurrogate = (value: string): boolean => {
for (let index = 0; index < value.length; index += 1) {
@@ -69,8 +74,8 @@ const projectSchema = z.object({
if (project.visibility === 'private' && project.url !== undefined) {
context.addIssue({ code: 'custom', message: 'A private project may not include a url.', path: ['url'] });
}
if (project.visibility === 'private' && project.title !== 'Private project') {
context.addIssue({ code: 'custom', message: 'Private project title must be generic.', path: ['title'] });
if (project.visibility === 'private' && !approvedPrivateProjectTitles.includes(project.title as (typeof approvedPrivateProjectTitles)[number])) {
context.addIssue({ code: 'custom', message: 'Private project title must use an approved generic title.', path: ['title'] });
}
});
@@ -111,7 +116,7 @@ const portfolioContentSchema = z.object({
}).strict(),
toolkit: z.array(z.object({
title: visibleString,
technologies: z.array(z.object({ name: visibleString, assetId: opaqueId }).strict()).min(1),
technologies: z.array(z.object({ name: visibleString, assetId: opaqueId.optional() }).strict()).min(1),
}).strict()).min(1),
contact: z.object({
email: z.email(),
@@ -131,7 +136,7 @@ const portfolioContentSchema = z.object({
metadata: z.object({
title: visibleString,
description: visibleString,
socialPreviewAssetId: opaqueId,
socialPreviewAssetId: opaqueId.optional(),
}).strict(),
sceneDescription: visibleString,
}).strict().superRefine((content, context) => {
@@ -192,7 +197,7 @@ const publicAssetSchema = z.object({
context.addIssue({ code: 'custom', message: 'Video assets require dimensions and duration.', path: ['durationMs'] });
}
});
const publicAssetManifestSchema = z.object({ assets: z.array(publicAssetSchema).min(1) }).strict().superRefine((manifest, context) => {
const publicAssetManifestSchema = z.object({ assets: z.array(publicAssetSchema) }).strict().superRefine((manifest, context) => {
const ids = manifest.assets.map((asset) => asset.id);
if (new Set(ids).size !== ids.length) {
context.addIssue({ code: 'custom', message: 'Duplicate asset ID in manifest.', path: ['assets'] });
@@ -233,8 +238,9 @@ export const validateProductionInputs = (
const referencedAssetIds = [
...content.projects.selected.flatMap((project) => project.assetIds),
...content.projects.additional.flatMap((project) => project.assetIds),
...content.toolkit.flatMap((group) => group.technologies.map((technology) => technology.assetId)),
content.metadata.socialPreviewAssetId,
...content.toolkit.flatMap((group) => group.technologies.flatMap((technology) =>
technology.assetId === undefined ? [] : [technology.assetId])),
...(content.metadata.socialPreviewAssetId === undefined ? [] : [content.metadata.socialPreviewAssetId]),
];
for (const assetId of referencedAssetIds) {
if (!manifestIds.has(assetId)) {