/**
* 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,
}
}