ChristopherJKoen's picture
Add page template system and UI
25058c7
import type { JobsheetSection, Page } from "../types/session";
export type FlatPage = {
sectionId: string;
sectionIndex: number;
sectionTitle?: string;
pageIndex: number;
page: Page;
flatIndex: number;
};
const MAX_PHOTOS_PRIMARY_PAGE = 2;
const MAX_PHOTOS_CONTINUATION_PAGE = 6;
function cloneTemplate(template?: Page["template"]): Page["template"] {
if (!template) return undefined;
return { ...template };
}
function normalizePhotoIds(photoIds?: string[]): string[] {
if (!Array.isArray(photoIds)) return [];
return photoIds.filter((value) => Boolean(value));
}
function buildPhotoContinuation(source: Page, photoIds: string[]): Page {
return {
items: [],
template: cloneTemplate(source.template),
photo_ids: photoIds,
photo_layout: source.photo_layout,
photo_order_locked: source.photo_order_locked,
variant: "photos",
};
}
function splitPagePhotos(page: Page): Page[] {
const items = Array.isArray(page.items) ? page.items : [];
const normalized: Page = { ...page, items };
if (normalized.blank) {
return [normalized];
}
const photoIds = normalizePhotoIds(normalized.photo_ids);
if (!photoIds.length) {
return [normalized];
}
if (normalized.variant === "photos") {
const chunks: string[][] = [];
for (let i = 0; i < photoIds.length; i += MAX_PHOTOS_CONTINUATION_PAGE) {
chunks.push(photoIds.slice(i, i + MAX_PHOTOS_CONTINUATION_PAGE));
}
if (chunks.length <= 1) {
return [
{
...normalized,
photo_ids: chunks[0] ?? [],
variant: "photos",
},
];
}
return chunks.map((chunk, idx) => {
if (idx === 0) return { ...normalized, photo_ids: chunk, variant: "photos" };
return buildPhotoContinuation(normalized, chunk);
});
}
const baseChunk = photoIds.slice(0, MAX_PHOTOS_PRIMARY_PAGE);
const extraIds = photoIds.slice(MAX_PHOTOS_PRIMARY_PAGE);
const continuationChunks: string[][] = [];
for (let i = 0; i < extraIds.length; i += MAX_PHOTOS_CONTINUATION_PAGE) {
continuationChunks.push(extraIds.slice(i, i + MAX_PHOTOS_CONTINUATION_PAGE));
}
const basePage: Page = {
...normalized,
photo_ids: baseChunk,
variant: normalized.variant ?? "full",
};
if (!continuationChunks.length) {
return [basePage];
}
const extraPages = continuationChunks.map((chunk) =>
buildPhotoContinuation(normalized, chunk),
);
return [basePage, ...extraPages];
}
export function ensureSections(sections?: JobsheetSection[]): JobsheetSection[] {
if (sections && sections.length) {
return sections.map((section, index) => {
const basePages = section.pages?.length ? section.pages : [{ items: [] }];
const normalizedPages = basePages.flatMap((page) => splitPagePhotos(page));
return {
id: section.id,
title: section.title ?? "Section",
pages: normalizedPages.length ? normalizedPages : [{ items: [] }],
};
});
}
return [
{
id: crypto.randomUUID(),
title: "Section 1",
pages: [{ items: [] }],
},
];
}
export function flattenSections(sections: JobsheetSection[]): FlatPage[] {
const result: FlatPage[] = [];
sections.forEach((section, sectionIndex) => {
const pages = section.pages ?? [];
pages.forEach((page, pageIndex) => {
result.push({
sectionId: section.id,
sectionIndex,
sectionTitle: section.title ?? `Section ${sectionIndex + 1}`,
pageIndex,
page,
flatIndex: result.length,
});
});
});
return result;
}
export function replacePage(
sections: JobsheetSection[],
sectionIndex: number,
pageIndex: number,
nextPage: Page,
): JobsheetSection[] {
const next = sections.map((section, sIdx) => {
if (sIdx !== sectionIndex) return section;
const nextPages = [...(section.pages ?? [])];
nextPages[pageIndex] = nextPage;
return { ...section, pages: nextPages };
});
return ensureSections(next);
}
export function insertPage(
sections: JobsheetSection[],
sectionIndex: number,
pageIndex: number,
page: Page,
): JobsheetSection[] {
const next = sections.map((section, sIdx) => {
if (sIdx !== sectionIndex) return section;
const nextPages = [...(section.pages ?? [])];
const safeIndex = Math.max(0, Math.min(pageIndex, nextPages.length));
nextPages.splice(safeIndex, 0, page);
return { ...section, pages: nextPages };
});
return ensureSections(next);
}
export function removePage(
sections: JobsheetSection[],
sectionIndex: number,
pageIndex: number,
): JobsheetSection[] {
const next = sections.map((section, sIdx) => {
if (sIdx !== sectionIndex) return section;
const nextPages = [...(section.pages ?? [])];
if (nextPages.length <= 1) return section;
nextPages.splice(pageIndex, 1);
return { ...section, pages: nextPages.length ? nextPages : [{ items: [] }] };
});
return ensureSections(next);
}