/** * useMediaCarousel Composable * * Extracts media carousel items from text content keys following the pattern: * `{prefix}.{N}.url`, `{prefix}.{N}.type`, `{prefix}.{N}.alt` * * @example * ```vue * * * * ``` */ import { computed, type ComputedRef } from 'vue' import { isCdnAssetUrl } from '@duffcloudservices/cms-core' /** * Media carousel item representing an image, video, or embed */ export interface MediaCarouselItem { /** URL to the image, video file, or embed URL */ url: string /** * Type of media: 'image', 'video' (direct file), 'youtube', or 'instagram'. * Render `youtube`/`instagram` items through the click-to-load facade * `LiteMediaEmbed` (`@duffcloudservices/cms/lite-media-embed`) so the heavy * player iframe loads only on user interaction — never eagerly. */ type: 'image' | 'video' | 'youtube' | 'instagram' /** Accessibility alt text */ alt?: string /** * Whether this image has responsive CDN variants available. * Automatically set to `true` when the URL matches the DCS CDN asset pattern. * Components rendering the carousel should use `` when this is `true`. */ responsive?: boolean } /** * Configuration for useMediaCarousel composable */ export interface UseMediaCarouselConfig { /** Key prefix for carousel items (e.g., 'hero.media-carousel') */ prefix: string /** The t() function from useTextContent */ t: (key: string, fallback?: string) => string /** Default items to use if no content keys are found */ defaults?: MediaCarouselItem[] /** Maximum number of items to look for (default: 10) */ maxItems?: number } /** * Return type for useMediaCarousel composable */ export interface UseMediaCarouselReturn { /** Computed array of media carousel items */ items: ComputedRef /** Whether any items were found from content keys */ hasItems: ComputedRef /** Number of items in the carousel */ count: ComputedRef } /** * Extract media carousel items from text content keys. * * Looks for keys in the format: * - `{prefix}.{N}.url` - Required URL for the media * - `{prefix}.{N}.type` - Type: 'image' or 'video' (defaults to 'image') * - `{prefix}.{N}.alt` - Alt text for accessibility * * Items are sorted by index (0, 1, etc.) and only included if they have a valid URL. * * @param config - Configuration object * @returns Media carousel helpers and state */ export function useMediaCarousel(config: UseMediaCarouselConfig): UseMediaCarouselReturn { const { prefix, t, defaults = [], maxItems = 10, } = config const items = computed(() => { const result: MediaCarouselItem[] = [] // Look for items from 0 to maxItems for (let i = 0; i < maxItems; i++) { const urlKey = `${prefix}.${i}.url` const typeKey = `${prefix}.${i}.type` const altKey = `${prefix}.${i}.alt` // Use a sentinel value to detect if the key exists const url = t(urlKey, '') // Skip if no URL (key doesn't exist or is empty) if (!url || url === urlKey) { continue } const typeValue = t(typeKey, 'image') // Parse type value - support image, video, youtube, instagram let type: 'image' | 'video' | 'youtube' | 'instagram' = 'image' if (typeValue === 'video') type = 'video' else if (typeValue === 'youtube') type = 'youtube' else if (typeValue === 'instagram') type = 'instagram' const alt = t(altKey, '') result.push({ url, type, alt: alt && alt !== altKey ? alt : undefined, // Flag CDN-hosted images as responsive so carousel components // can render them with automatically responsive: type === 'image' && isCdnAssetUrl(url), }) } // If no items found from content keys, use defaults if (result.length === 0) { return defaults } return result }) const hasItems = computed(() => items.value.length > 0) const count = computed(() => items.value.length) return { items, hasItems, count, } }