feat: present cleared accessible project media

This commit is contained in:
2026-09-02 06:50:00 +03:00
parent dfd56f0a48
commit f953905a4b
12 changed files with 832 additions and 4 deletions

View File

@@ -58,6 +58,46 @@ const navigationItemSchema = z.object({
id: z.enum(canonicalNavigationIds),
label: visibleString,
}).strict();
const mediaPresentationBase = {
assetId: opaqueId,
alt: visibleString,
caption: visibleString.optional(),
longDescription: visibleString.optional(),
};
const mediaTableSchema = z.object({
caption: visibleString,
columns: z.array(visibleString).min(2),
rows: z.array(z.array(visibleString).min(2)).min(1),
}).strict().superRefine((table, context) => {
for (const [index, row] of table.rows.entries()) {
if (row.length !== table.columns.length) {
context.addIssue({ code: 'custom', message: 'Every chart table row must match the column count.', path: ['rows', index] });
}
}
});
const mediaPresentationSchema = z.discriminatedUnion('kind', [
z.object({ kind: z.literal('image'), ...mediaPresentationBase }).strict(),
z.object({
kind: z.literal('video'),
...mediaPresentationBase,
fallbackAssetId: opaqueId.optional(),
posterAssetId: opaqueId,
caption: visibleString,
longDescription: visibleString,
}).strict(),
z.object({
kind: z.literal('chart'),
...mediaPresentationBase,
caption: visibleString,
chart: z.object({
variables: visibleString,
trend: visibleString,
conclusion: visibleString,
table: mediaTableSchema.optional(),
}).strict(),
}).strict(),
]);
const projectSchema = z.object({
id: opaqueId,
title: visibleString,
@@ -70,6 +110,7 @@ const projectSchema = z.object({
outcome: visibleString,
url: httpUrl.optional(),
assetIds: z.array(opaqueId),
media: z.array(mediaPresentationSchema).optional(),
}).strict().superRefine((project, context) => {
if (project.visibility === 'private' && project.url !== undefined) {
context.addIssue({ code: 'custom', message: 'A private project may not include a url.', path: ['url'] });
@@ -77,6 +118,21 @@ const projectSchema = z.object({
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'] });
}
if (project.media !== undefined) {
const mediaAssetIds = project.media.flatMap((media) => media.kind === 'video'
? [media.assetId, ...(media.fallbackAssetId === undefined ? [] : [media.fallbackAssetId]), media.posterAssetId]
: [media.assetId]);
const declared = [...project.assetIds].sort();
const presented = [...mediaAssetIds].sort();
if (new Set(declared).size !== declared.length || new Set(presented).size !== presented.length ||
JSON.stringify(declared) !== JSON.stringify(presented)) {
context.addIssue({
code: 'custom',
message: 'Project media must have an exact, duplicate-free asset closure.',
path: ['assetIds'],
});
}
}
});
const portfolioContentSchema = z.object({