Files

126 lines
3.8 KiB
TypeScript

import type { MenuItem } from '@/domain/menu/entities';
import {
MEDIA_FOLDERS,
SITE_ASSETS,
mediaUrl,
} from '@/infrastructure/assets/site-assets';
export type PosterVariant = 'standard' | 'optimized';
/** Strip .mp4 extension from a video filename */
export function videoBaseName(videoFilename: string): string {
return videoFilename.replace(/\.mp4$/i, '');
}
/** Full URL for a dish image filename (e.g. "butter-chicken.jpg") */
export function dishImageUrl(filename: string): string {
return mediaUrl(MEDIA_FOLDERS.dishes, filename);
}
/** Poster URL derived from a video filename */
export function dishPosterFromVideo(
videoFilename: string,
variant: PosterVariant = 'standard'
): string {
const base = videoBaseName(videoFilename);
const suffix = variant === 'optimized' ? '-optimized-poster.jpg' : '-poster.jpg';
return mediaUrl(MEDIA_FOLDERS.dishes, `${base}${suffix}`);
}
/** Primary logo used in navbar, footer, login, etc. */
export function logoUrl(): string {
return SITE_ASSETS.logo.primary;
}
/** Hero banner video sources for homepage */
export function heroBannerSources() {
return {
mobileWebm: SITE_ASSETS.banner.mobileWebm,
mobileMp4: SITE_ASSETS.banner.mobileMp4,
desktopMp4: SITE_ASSETS.banner.desktopMp4,
};
}
/** Chef expression images for PlayfulHeroScene */
export function chefExpressionUrl(expression: 'wink' | 'smile' | 'normal'): string {
const map = {
wink: SITE_ASSETS.animation.chefWink,
smile: SITE_ASSETS.animation.chefSmile,
normal: SITE_ASSETS.animation.chefNormal,
};
return map[expression];
}
/** Resolve primary poster path for a menu item */
export function getMenuPosterSrc(
item: MenuItem,
variant: PosterVariant = 'standard'
): string {
// Prefer static dish photo when available (sweets use synced .jpg, not -poster.jpg)
if (item.image) {
return dishImageUrl(item.image);
}
if (item.video) {
return dishPosterFromVideo(item.video, variant);
}
return variant === 'optimized'
? SITE_ASSETS.dishes.defaultPoster
: SITE_ASSETS.fallbacks.dishPoster;
}
/** Poster candidates for progressive fallback in UI `<img onError>` */
export function getMenuPosterCandidates(item: MenuItem): string[] {
const base = item.video ? videoBaseName(item.video) : '';
const candidates: string[] = [];
if (item.image) candidates.push(dishImageUrl(item.image));
if (item.video) {
candidates.push(
dishPosterFromVideo(item.video, 'standard'),
dishPosterFromVideo(item.video, 'optimized'),
mediaUrl(MEDIA_FOLDERS.dishes, `${base}.jpg`),
);
}
candidates.push(
SITE_ASSETS.dishes.defaultPoster,
SITE_ASSETS.fallbacks.dishPoster,
SITE_ASSETS.fallbacks.logo,
);
return [...new Set(candidates)];
}
/** Full video URL for a menu item */
export function getMenuVideoSrc(item: MenuItem): string | null {
if (!item.video) return null;
return mediaUrl(MEDIA_FOLDERS.videos, item.video);
}
/** Optimized + fallback video sources for kiosk displays */
export function getMenuVideoSources(videoFilename: string) {
if (!videoFilename) {
return { webm: '', mp4: '', fallback: '' };
}
const base = videoBaseName(videoFilename);
return {
webm: mediaUrl(MEDIA_FOLDERS.videos, `${base}-optimized.webm`),
mp4: mediaUrl(MEDIA_FOLDERS.videos, `${base}-optimized.mp4`),
fallback: mediaUrl(MEDIA_FOLDERS.videos, `${base}.mp4`),
};
}
/** Apply next fallback src in an img onError handler */
export function applyNextImageFallback(
target: HTMLImageElement,
candidates: string[],
currentSrc: string
): void {
const currentIndex = candidates.indexOf(currentSrc);
const nextIndex = currentIndex >= 0 ? currentIndex + 1 : 0;
if (nextIndex < candidates.length) {
target.src = candidates[nextIndex];
}
}
// Re-export registry for direct access when needed
export { SITE_ASSETS, MEDIA_FOLDERS, mediaUrl } from '@/infrastructure/assets/site-assets';