feat: present cleared accessible project media
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -11,6 +11,47 @@ export type SectionId =
|
||||
export type ProjectVisibility = 'public' | 'private';
|
||||
export type ProjectMaturity = 'learning' | 'experimental' | 'prototype' | 'work-in-progress' | 'active';
|
||||
|
||||
export interface MediaPresentationBase {
|
||||
assetId: string;
|
||||
alt: string;
|
||||
caption?: string;
|
||||
longDescription?: string;
|
||||
}
|
||||
|
||||
export interface ImageMediaPresentation extends MediaPresentationBase {
|
||||
kind: 'image';
|
||||
}
|
||||
|
||||
export interface VideoMediaPresentation extends MediaPresentationBase {
|
||||
kind: 'video';
|
||||
fallbackAssetId?: string;
|
||||
posterAssetId: string;
|
||||
caption: string;
|
||||
longDescription: string;
|
||||
}
|
||||
|
||||
export interface ChartValueTable {
|
||||
caption: string;
|
||||
columns: readonly string[];
|
||||
rows: ReadonlyArray<readonly string[]>;
|
||||
}
|
||||
|
||||
export interface ChartMediaPresentation extends MediaPresentationBase {
|
||||
kind: 'chart';
|
||||
caption: string;
|
||||
chart: {
|
||||
variables: string;
|
||||
trend: string;
|
||||
conclusion: string;
|
||||
table?: ChartValueTable;
|
||||
};
|
||||
}
|
||||
|
||||
export type MediaPresentation =
|
||||
| ImageMediaPresentation
|
||||
| VideoMediaPresentation
|
||||
| ChartMediaPresentation;
|
||||
|
||||
export interface ExperienceRecord {
|
||||
id: string;
|
||||
role: string;
|
||||
@@ -52,6 +93,7 @@ export interface PublicProject {
|
||||
outcome: string;
|
||||
url?: string;
|
||||
assetIds: readonly string[];
|
||||
media?: readonly MediaPresentation[];
|
||||
}
|
||||
|
||||
export interface ToolkitTechnology {
|
||||
|
||||
Reference in New Issue
Block a user