310 lines
9.6 KiB
TypeScript
310 lines
9.6 KiB
TypeScript
/**
|
|
* extractPageLinks Utility
|
|
*
|
|
* Extracts synthetic page links and preload elements from tour pages' ui_schema_json.
|
|
* Used by both constructor and RuntimePresentation for consistent preload behavior.
|
|
*/
|
|
|
|
import { PRELOAD_CONFIG } from '../config/preload.config';
|
|
import type { PreloadPageLink, PreloadElement } from '../types/preload';
|
|
|
|
interface PageWithSchema {
|
|
id: string;
|
|
slug?: string;
|
|
ui_schema_json?: string | Record<string, unknown>;
|
|
}
|
|
|
|
interface ExtractResult {
|
|
pageLinks: PreloadPageLink[];
|
|
preloadElements: PreloadElement[];
|
|
}
|
|
|
|
/**
|
|
* Extract asset URL fields from an element using PRELOAD_CONFIG.
|
|
* Returns an object with all asset fields found in the element.
|
|
*/
|
|
function extractAssetFields(
|
|
element: Record<string, unknown>,
|
|
): Record<string, unknown> {
|
|
const {
|
|
all: allFields,
|
|
nested: nestedFields,
|
|
nestedUrlFields,
|
|
} = PRELOAD_CONFIG.assetFields;
|
|
const contentObj: Record<string, unknown> = {};
|
|
|
|
// Extract top-level asset fields
|
|
(allFields as readonly string[]).forEach((field) => {
|
|
const value = element[field];
|
|
if (value !== undefined && value !== '') {
|
|
contentObj[field] = value;
|
|
}
|
|
});
|
|
|
|
// Extract nested fields (e.g., galleryCards, carouselSlides)
|
|
(nestedFields as readonly string[]).forEach((field) => {
|
|
const nested = element[field];
|
|
if (Array.isArray(nested)) {
|
|
// Extract URLs from nested items
|
|
const filteredNested = nested
|
|
.map((item: Record<string, unknown>) => {
|
|
const nestedContent: Record<string, unknown> = {};
|
|
(nestedUrlFields as readonly string[]).forEach((urlField) => {
|
|
if (item[urlField] !== undefined && item[urlField] !== '') {
|
|
nestedContent[urlField] = item[urlField];
|
|
}
|
|
});
|
|
return Object.keys(nestedContent).length > 0 ? nestedContent : null;
|
|
})
|
|
.filter(Boolean);
|
|
if (filteredNested.length > 0) {
|
|
contentObj[field] = filteredNested;
|
|
}
|
|
}
|
|
});
|
|
|
|
return contentObj;
|
|
}
|
|
|
|
/**
|
|
* Parse ui_schema_json safely.
|
|
*/
|
|
function parseUiSchema(
|
|
uiSchemaJson: string | Record<string, unknown> | undefined,
|
|
): Record<string, unknown> | null {
|
|
if (!uiSchemaJson) return null;
|
|
try {
|
|
return typeof uiSchemaJson === 'string'
|
|
? JSON.parse(uiSchemaJson)
|
|
: uiSchemaJson;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract page links and preload elements from pages' ui_schema_json.
|
|
*
|
|
* This builds:
|
|
* 1. Synthetic page links for the neighbor graph (enables preloading of connected pages)
|
|
* 2. Preload elements with asset URLs for the preload queue
|
|
*
|
|
* @param pages - Array of pages with ui_schema_json
|
|
* @param allPages - Optional: all pages for slug-to-id resolution. If not provided, uses `pages`.
|
|
* @returns Object with pageLinks and preloadElements arrays
|
|
*
|
|
* @example
|
|
* const { pageLinks, preloadElements } = extractPageLinksAndElements(pages);
|
|
* const preloadOrchestrator = usePreloadOrchestrator({
|
|
* pages,
|
|
* pageLinks,
|
|
* elements: preloadElements,
|
|
* currentPageId,
|
|
* });
|
|
*/
|
|
export function extractPageLinksAndElements(
|
|
pages: PageWithSchema[],
|
|
allPages?: PageWithSchema[],
|
|
): ExtractResult {
|
|
const pagesForLookup = allPages || pages;
|
|
const pageLinks: PreloadPageLink[] = [];
|
|
const preloadElements: PreloadElement[] = [];
|
|
|
|
// Build slug-to-id map for resolving targetPageSlug
|
|
const slugToIdMap = new Map<string, string>();
|
|
pagesForLookup.forEach((page) => {
|
|
if (page.slug) {
|
|
slugToIdMap.set(page.slug, page.id);
|
|
}
|
|
});
|
|
|
|
pages.forEach((page) => {
|
|
const uiSchema = parseUiSchema(page.ui_schema_json);
|
|
if (!uiSchema) return;
|
|
|
|
const pageElements = Array.isArray(uiSchema.elements)
|
|
? (uiSchema.elements as Record<string, unknown>[])
|
|
: [];
|
|
|
|
pageElements.forEach((el) => {
|
|
// Build preload element with asset URLs
|
|
const contentObj = extractAssetFields(el);
|
|
if (Object.keys(contentObj).length > 0) {
|
|
preloadElements.push({
|
|
id:
|
|
String(el.id || '') ||
|
|
`element-${page.id}-${Math.random().toString(36).slice(2)}`,
|
|
pageId: page.id,
|
|
element_type: String(el.type || ''),
|
|
content_json: JSON.stringify(contentObj),
|
|
});
|
|
}
|
|
|
|
// Build synthetic page link for navigation elements
|
|
const targetSlug =
|
|
el.targetPageSlug && typeof el.targetPageSlug === 'string'
|
|
? el.targetPageSlug
|
|
: '';
|
|
const legacyTargetId =
|
|
el.targetPageId && typeof el.targetPageId === 'string'
|
|
? el.targetPageId
|
|
: '';
|
|
|
|
// Resolve slug to page ID (prefer slug, fall back to legacy ID)
|
|
let resolvedTargetPageId = '';
|
|
if (targetSlug) {
|
|
resolvedTargetPageId = slugToIdMap.get(targetSlug) || '';
|
|
} else if (legacyTargetId) {
|
|
// Legacy: targetPageId might be a slug or an ID
|
|
resolvedTargetPageId =
|
|
slugToIdMap.get(legacyTargetId) || legacyTargetId;
|
|
}
|
|
|
|
if (resolvedTargetPageId && resolvedTargetPageId !== page.id) {
|
|
pageLinks.push({
|
|
id: `synthetic-${page.id}-${el.id || preloadElements.length}`,
|
|
from_pageId: page.id,
|
|
to_pageId: resolvedTargetPageId,
|
|
is_active: true,
|
|
transition:
|
|
el.transitionVideoUrl && typeof el.transitionVideoUrl === 'string'
|
|
? {
|
|
id: `transition-${el.id || preloadElements.length}`,
|
|
video_url: el.transitionVideoUrl,
|
|
}
|
|
: undefined,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
return { pageLinks, preloadElements };
|
|
}
|
|
|
|
/**
|
|
* Extract only page links from pages (lightweight - no asset extraction).
|
|
* Used for building navigation graph without loading all asset URLs.
|
|
*
|
|
* This is more efficient than extractPageLinksAndElements when you only need
|
|
* the navigation structure (e.g., for determining neighbors).
|
|
*
|
|
* @param pages - Array of pages with ui_schema_json
|
|
* @param allPages - Optional: all pages for slug-to-id resolution. If not provided, uses `pages`.
|
|
* @returns Array of page links for navigation graph
|
|
*/
|
|
export function extractPageLinksOnly(
|
|
pages: PageWithSchema[],
|
|
allPages?: PageWithSchema[],
|
|
): PreloadPageLink[] {
|
|
const pagesForLookup = allPages || pages;
|
|
const pageLinks: PreloadPageLink[] = [];
|
|
|
|
// Build slug-to-id map for resolving targetPageSlug
|
|
const slugToIdMap = new Map<string, string>();
|
|
pagesForLookup.forEach((page) => {
|
|
if (page.slug) {
|
|
slugToIdMap.set(page.slug, page.id);
|
|
}
|
|
});
|
|
|
|
pages.forEach((page) => {
|
|
const uiSchema = parseUiSchema(page.ui_schema_json);
|
|
if (!uiSchema) return;
|
|
|
|
const pageElements = Array.isArray(uiSchema.elements)
|
|
? (uiSchema.elements as Record<string, unknown>[])
|
|
: [];
|
|
|
|
pageElements.forEach((el) => {
|
|
// Build synthetic page link for navigation elements
|
|
const targetSlug =
|
|
el.targetPageSlug && typeof el.targetPageSlug === 'string'
|
|
? el.targetPageSlug
|
|
: '';
|
|
const legacyTargetId =
|
|
el.targetPageId && typeof el.targetPageId === 'string'
|
|
? el.targetPageId
|
|
: '';
|
|
|
|
// Resolve slug to page ID (prefer slug, fall back to legacy ID)
|
|
let resolvedTargetPageId = '';
|
|
if (targetSlug) {
|
|
resolvedTargetPageId = slugToIdMap.get(targetSlug) || '';
|
|
} else if (legacyTargetId) {
|
|
// Legacy: targetPageId might be a slug or an ID
|
|
resolvedTargetPageId =
|
|
slugToIdMap.get(legacyTargetId) || legacyTargetId;
|
|
}
|
|
|
|
if (resolvedTargetPageId && resolvedTargetPageId !== page.id) {
|
|
pageLinks.push({
|
|
id: `synthetic-${page.id}-${el.id || Math.random().toString(36).slice(2)}`,
|
|
from_pageId: page.id,
|
|
to_pageId: resolvedTargetPageId,
|
|
is_active: true,
|
|
transition:
|
|
el.transitionVideoUrl && typeof el.transitionVideoUrl === 'string'
|
|
? {
|
|
id: `transition-${el.id || Math.random().toString(36).slice(2)}`,
|
|
video_url: el.transitionVideoUrl,
|
|
}
|
|
: undefined,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
return pageLinks;
|
|
}
|
|
|
|
/**
|
|
* Extract preload elements only for specified pages (on-demand).
|
|
* Used for progressive element loading - only parses ui_schema_json
|
|
* for pages that are actually needed (current + neighbors).
|
|
*
|
|
* @param pages - Array of pages with ui_schema_json
|
|
* @param pageIds - Array of page IDs to extract elements for
|
|
* @returns Array of preload elements for the specified pages
|
|
*
|
|
* @example
|
|
* // Extract elements only for current page and its neighbors
|
|
* const neighborIds = [currentPageId, ...getNeighborIds(currentPageId)];
|
|
* const elements = extractElementsForPages(pages, neighborIds);
|
|
*/
|
|
export function extractElementsForPages(
|
|
pages: PageWithSchema[],
|
|
pageIds: string[],
|
|
): PreloadElement[] {
|
|
const preloadElements: PreloadElement[] = [];
|
|
const pageIdSet = new Set(pageIds);
|
|
|
|
pages.forEach((page) => {
|
|
// Skip pages not in requested set
|
|
if (!pageIdSet.has(page.id)) return;
|
|
|
|
const uiSchema = parseUiSchema(page.ui_schema_json);
|
|
if (!uiSchema) return;
|
|
|
|
const pageElements = Array.isArray(uiSchema.elements)
|
|
? (uiSchema.elements as Record<string, unknown>[])
|
|
: [];
|
|
|
|
pageElements.forEach((el) => {
|
|
// Build preload element with asset URLs
|
|
const contentObj = extractAssetFields(el);
|
|
if (Object.keys(contentObj).length > 0) {
|
|
preloadElements.push({
|
|
id:
|
|
String(el.id || '') ||
|
|
`element-${page.id}-${Math.random().toString(36).slice(2)}`,
|
|
pageId: page.id,
|
|
element_type: String(el.type || ''),
|
|
content_json: JSON.stringify(contentObj),
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
return preloadElements;
|
|
}
|