/** * Constructor Helpers * * Utility functions for the constructor page. * Extracted from constructor.tsx for reusability. */ import type { CanvasElement, ConstructorAsset as ProjectAsset, AssetOption, } from '../types/constructor'; import { isGalleryElementType, isCarouselElementType, isTooltipElementType, isDescriptionElementType, isNavigationElementType, isMediaElementType, getNavigationButtonLabel, } from './elementDefaults'; /** * Clamp a number between min and max */ export const clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max); /** * Get trimmed CSS value as string. * Handles null/undefined gracefully. */ export const getTrimmedCssValue = (value: unknown): string => { if (value === null || value === undefined) return ''; return String(value).trim(); }; /** * Format asset label for display in select dropdowns. * Shows asset name with source path suffix. */ export const getAssetLabel = (asset: ProjectAsset): string => { const baseName = asset.name?.trim() || 'Untitled asset'; const source = String(asset.storage_key || asset.cdn_url || '').trim(); return `${baseName}${source ? ` · ${source}` : ''}`; }; /** * Get asset source value (storage_key or cdn_url). * Used as the value in asset select dropdowns. */ export const getAssetSourceValue = (asset: ProjectAsset): string => String(asset.storage_key || asset.cdn_url || '').trim(); /** * Check if an asset is likely a background image based on name/type. * Used to filter assets for background image selection. */ export const isBackgroundImageAsset = (asset: ProjectAsset): boolean => { if (asset.type) return asset.type === 'background_image'; const normalizedName = String(asset.name || '').toLowerCase(); if (!normalizedName) return false; const hasBackgroundKeyword = /\bbackground\b|\bbg\b|backdrop|wallpaper/.test( normalizedName, ); const hasExcludedKeyword = /\bicon\b|\blogo\b/.test(normalizedName); return hasBackgroundKeyword && !hasExcludedKeyword; }; /** * Add current value as a fallback option if not already present. * Ensures the currently selected value is always available in the dropdown. */ export const addFallbackAssetOption = ( options: AssetOption[], value?: string, fallbackLabel?: string, ): AssetOption[] => { const normalizedValue = String(value || '').trim(); if (!normalizedValue) return options; if (options.some((option) => option.value === normalizedValue)) return options; return [ ...options, { value: normalizedValue, label: fallbackLabel || `Custom URL · ${normalizedValue}`, }, ]; }; /** * Get element button title for display in constructor menu. * Shows element-specific info like card/slide count. */ export const getElementButtonTitle = (element: CanvasElement): string => { if (isGalleryElementType(element.type)) { return `${element.label} (${element.galleryCards?.length || 0})`; } if (isCarouselElementType(element.type)) { return `${element.label} (${element.carouselSlides?.length || 0})`; } if (isTooltipElementType(element.type)) return element.tooltipTitle ?? ''; if (isDescriptionElementType(element.type)) return element.descriptionTitle ?? ''; if (isNavigationElementType(element.type)) { type NavigationElementType = 'navigation_next' | 'navigation_prev'; return ( element.navLabel?.trim() || getNavigationButtonLabel(element.type as NavigationElementType) ); } if (isMediaElementType(element.type) && element.mediaUrl) { return `${element.label} · configured`; } return element.label; }; /** * Build asset options for a specific asset type. * Filters assets by type and creates label/value pairs. */ export const buildAssetOptions = ( assets: ProjectAsset[], assetType: 'image' | 'video' | 'audio', additionalFilter?: (asset: ProjectAsset) => boolean, ): AssetOption[] => { return assets .filter((asset) => { if (asset.asset_type !== assetType) return false; if (!getAssetSourceValue(asset)) return false; if (additionalFilter && !additionalFilter(asset)) return false; return true; }) .map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset), })); }; /** * Build background image asset options. * Filters for image assets that appear to be backgrounds. */ export const buildBackgroundImageOptions = ( assets: ProjectAsset[], ): AssetOption[] => { return buildAssetOptions(assets, 'image', isBackgroundImageAsset); }; /** * Build video asset options. */ export const buildVideoAssetOptions = ( assets: ProjectAsset[], ): AssetOption[] => { return buildAssetOptions(assets, 'video'); }; /** * Build audio asset options. */ export const buildAudioAssetOptions = ( assets: ProjectAsset[], ): AssetOption[] => { return buildAssetOptions(assets, 'audio'); }; /** * Build transition video asset options. * Prefers assets marked as type='transition', falls back to tagged or all videos. */ export const buildTransitionVideoOptions = ( assets: ProjectAsset[], ): AssetOption[] => { // First try assets marked as transition type const typedAssets = assets .filter( (asset) => asset.type === 'transition' && asset.asset_type === 'video' && getAssetSourceValue(asset), ) .map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset), })); if (typedAssets.length > 0) return typedAssets; // Fall back to assets with [TRANSITION] tag in name const taggedAssets = assets .filter( (asset) => asset.asset_type === 'video' && getAssetSourceValue(asset) && /\[TRANSITION\]/i.test(String(asset.name || '')), ) .map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset), })); if (taggedAssets.length > 0) return taggedAssets; // Fall back to all video assets return buildVideoAssetOptions(assets); }; /** * Build icon asset options. * Filters for image assets marked as type='icon'. */ export const buildIconAssetOptions = ( assets: ProjectAsset[], ): AssetOption[] => { return assets .filter( (asset) => asset.type === 'icon' && asset.asset_type === 'image' && getAssetSourceValue(asset), ) .map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset), })); }; /** * Build all image asset options (no filtering). */ export const buildImageAssetOptions = ( assets: ProjectAsset[], ): AssetOption[] => { return buildAssetOptions(assets, 'image'); }; /** * Extract numeric value from CSS value string. * Used for compact style inputs that expect raw numbers. * * @example * extractNumericValue('24vw'); // '24' * extractNumericValue('100px'); // '100' * extractNumericValue(''); // '' */ export const extractNumericValue = (value?: string): string => { if (!value) return ''; const match = String(value).match(/^(-?\d+\.?\d*)/); return match ? match[1] : ''; }; /** * Build page name lookup map by ID. */ export const buildPageNameById = ( pages: Array<{ id: string; name?: string }>, ): Record => { const acc: Record = {}; pages.forEach((page, index) => { acc[String(page.id)] = page.name || `Page ${index + 1}`; }); return acc; }; /** * Build page name lookup map by slug. */ export const buildPageNameBySlug = ( pages: Array<{ slug?: string; name?: string }>, ): Record => { const acc: Record = {}; pages.forEach((page, index) => { if (page.slug) { acc[String(page.slug)] = page.name || `Page ${index + 1}`; } }); return acc; };