364 lines
14 KiB
TypeScript
364 lines
14 KiB
TypeScript
import { Link } from 'react-router-dom';
|
|
import type { HeroSectionPayload } from '@homaroma/types';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { getLocalizedField } from '../utils/localization';
|
|
|
|
interface DynamicHeroProps {
|
|
payload: HeroSectionPayload;
|
|
}
|
|
|
|
// Map 9-box grid position to mobile flex classes
|
|
export const getHeroMobilePositionClasses = (pos: string) => {
|
|
switch (pos) {
|
|
case 'top-left':
|
|
return {
|
|
wrapper: 'justify-start items-start text-left pt-20 pb-12 pl-6 pr-6',
|
|
content: 'items-start text-left',
|
|
actions: 'items-start justify-start'
|
|
};
|
|
case 'top-center':
|
|
return {
|
|
wrapper: 'justify-start items-center text-center pt-20 pb-12 px-6',
|
|
content: 'items-center text-center',
|
|
actions: 'items-center justify-center'
|
|
};
|
|
case 'top-right':
|
|
return {
|
|
wrapper: 'justify-start items-end text-right pt-20 pb-12 pr-6 pl-6',
|
|
content: 'items-end text-right',
|
|
actions: 'items-end justify-end'
|
|
};
|
|
case 'center-left':
|
|
return {
|
|
wrapper: 'justify-center items-start text-left py-16 pl-6 pr-6',
|
|
content: 'items-start text-left',
|
|
actions: 'items-start justify-start'
|
|
};
|
|
case 'center-right':
|
|
return {
|
|
wrapper: 'justify-center items-end text-right py-16 pr-6 pl-6',
|
|
content: 'items-end text-right',
|
|
actions: 'items-end justify-end'
|
|
};
|
|
case 'bottom-left':
|
|
return {
|
|
wrapper: 'justify-end items-start text-left pt-12 pb-16 pl-6 pr-6',
|
|
content: 'items-start text-left',
|
|
actions: 'items-start justify-start'
|
|
};
|
|
case 'bottom-center':
|
|
return {
|
|
wrapper: 'justify-end items-center text-center pt-12 pb-16 px-6',
|
|
content: 'items-center text-center',
|
|
actions: 'items-center justify-center'
|
|
};
|
|
case 'bottom-right':
|
|
return {
|
|
wrapper: 'justify-end items-end text-right pt-12 pb-16 pr-6 pl-6',
|
|
content: 'items-end text-right',
|
|
actions: 'items-end justify-end'
|
|
};
|
|
case 'center':
|
|
default:
|
|
return {
|
|
wrapper: 'justify-center items-center text-center py-16 px-6',
|
|
content: 'items-center text-center',
|
|
actions: 'items-center justify-center'
|
|
};
|
|
}
|
|
};
|
|
|
|
// Map 9-box grid position to desktop (md:) flex classes
|
|
export const getHeroDesktopPositionClasses = (pos: string) => {
|
|
switch (pos) {
|
|
case 'top-left':
|
|
return {
|
|
wrapper: 'md:justify-start md:items-start md:text-left md:pt-28 md:pb-12 md:pl-16 md:pr-6',
|
|
content: 'md:items-start md:text-left',
|
|
actions: 'md:items-start md:justify-start'
|
|
};
|
|
case 'top-center':
|
|
return {
|
|
wrapper: 'md:justify-start md:items-center md:text-center md:pt-28 md:pb-12 md:px-6',
|
|
content: 'md:items-center md:text-center',
|
|
actions: 'md:items-center md:justify-center'
|
|
};
|
|
case 'top-right':
|
|
return {
|
|
wrapper: 'md:justify-start md:items-end md:text-right md:pt-28 md:pb-12 md:pr-16 md:pl-6',
|
|
content: 'md:items-end md:text-right',
|
|
actions: 'md:items-end md:justify-end'
|
|
};
|
|
case 'center-left':
|
|
return {
|
|
wrapper: 'md:justify-center md:items-start md:text-left md:py-16 md:pl-16 md:pr-6',
|
|
content: 'md:items-start md:text-left',
|
|
actions: 'md:items-start md:justify-start'
|
|
};
|
|
case 'center-right':
|
|
return {
|
|
wrapper: 'md:justify-center md:items-end md:text-right md:py-16 md:pr-16 md:pl-6',
|
|
content: 'md:items-end md:text-right',
|
|
actions: 'md:items-end md:justify-end'
|
|
};
|
|
case 'bottom-left':
|
|
return {
|
|
wrapper: 'md:justify-end md:items-start md:text-left md:pt-12 md:pb-24 md:pl-16 md:pr-6',
|
|
content: 'md:items-start md:text-left',
|
|
actions: 'md:items-start md:justify-start'
|
|
};
|
|
case 'bottom-center':
|
|
return {
|
|
wrapper: 'md:justify-end md:items-center md:text-center md:pt-12 md:pb-24 md:px-6',
|
|
content: 'md:items-center md:text-center',
|
|
actions: 'md:items-center md:justify-center'
|
|
};
|
|
case 'bottom-right':
|
|
return {
|
|
wrapper: 'md:justify-end md:items-end md:text-right md:pt-12 md:pb-24 md:pr-16 md:pl-6',
|
|
content: 'md:items-end md:text-right',
|
|
actions: 'md:items-end md:justify-end'
|
|
};
|
|
case 'center':
|
|
default:
|
|
return {
|
|
wrapper: 'md:justify-center md:items-center md:text-center md:py-16 md:px-6',
|
|
content: 'md:items-center md:text-center',
|
|
actions: 'md:items-center md:justify-center'
|
|
};
|
|
}
|
|
};
|
|
|
|
export function getHeroPositionClasses(mobilePos: string, desktopPos: string) {
|
|
const mClasses = getHeroMobilePositionClasses(mobilePos);
|
|
const dClasses = getHeroDesktopPositionClasses(desktopPos);
|
|
return {
|
|
wrapperClasses: `${mClasses.wrapper} ${dClasses.wrapper}`,
|
|
contentClasses: `${mClasses.content} ${dClasses.content}`,
|
|
actionsClasses: `${mClasses.actions} ${dClasses.actions}`,
|
|
};
|
|
}
|
|
|
|
export function getHeroContainerWidthClasses(contentWidth?: string) {
|
|
switch (contentWidth) {
|
|
case 'full':
|
|
return 'w-full max-w-none px-6 md:px-12 lg:px-16';
|
|
case 'compact':
|
|
return 'w-full max-w-4xl mx-auto px-6';
|
|
case 'standard':
|
|
default:
|
|
return 'w-full max-w-7xl mx-auto px-6 md:px-10 lg:px-12';
|
|
}
|
|
}
|
|
|
|
export function getHeroTitleSizeClass(titleSize?: string): string {
|
|
switch (titleSize) {
|
|
case 'sm':
|
|
return 'ui-hero-title-sm';
|
|
case 'md':
|
|
return 'ui-hero-title-md';
|
|
case 'lg':
|
|
return 'ui-hero-title-lg';
|
|
case 'xl':
|
|
default:
|
|
return 'ui-hero-title-xl';
|
|
}
|
|
}
|
|
|
|
export default function DynamicHero({ payload }: DynamicHeroProps) {
|
|
const { t, i18n } = useTranslation();
|
|
const lang = i18n.language;
|
|
const { imageUrl, buttonLink } = payload;
|
|
const desktopPosition = (payload as any).buttonPosition || 'center';
|
|
const mobilePosition = (payload as any).mobileButtonPosition || desktopPosition;
|
|
const contentWidth = (payload as any).contentWidth || 'standard';
|
|
const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined;
|
|
|
|
// Localized values
|
|
const rawTagline = getLocalizedField(payload.tagline, lang);
|
|
const rawTitle = getLocalizedField(payload.title, lang);
|
|
const rawDescription = getLocalizedField(payload.description, lang);
|
|
const rawButtonText = getLocalizedField(payload.buttonText, lang);
|
|
|
|
// If fields are explicitly defined as empty strings, respect them as empty
|
|
const hasTagline = payload.tagline !== undefined && payload.tagline !== null ? !!rawTagline?.trim() : true;
|
|
const tagline = hasTagline ? (rawTagline || t('hero.defaultTagline')) : '';
|
|
|
|
const hasTitle = payload.title !== undefined && payload.title !== null ? !!rawTitle?.trim() : true;
|
|
const title = hasTitle ? (rawTitle || t('hero.defaultTitle')) : '';
|
|
|
|
const hasDescription = payload.description !== undefined && payload.description !== null ? !!rawDescription?.trim() : false;
|
|
const description = hasDescription ? rawDescription : '';
|
|
|
|
const buttonText = rawButtonText !== undefined && rawButtonText !== null
|
|
? rawButtonText.trim()
|
|
: t('hero.exploreCollection');
|
|
|
|
const textColorScheme = (payload as any).textColorScheme || 'light';
|
|
const isDarkText = textColorScheme === 'dark';
|
|
|
|
const renderTitle = (text: string) => {
|
|
if (!text) return null;
|
|
return text.split('[').map((part, i) => {
|
|
if (part.includes(']')) {
|
|
const [accent, rest] = part.split(']');
|
|
return (
|
|
<span key={i}>
|
|
<span className={`${isDarkText ? 'text-pop-pink' : 'text-pop-yellow'} font-medium italic`}>
|
|
{accent}
|
|
</span>
|
|
{rest}
|
|
</span>
|
|
);
|
|
}
|
|
return <span key={i}>{part}</span>;
|
|
});
|
|
};
|
|
|
|
const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(mobilePosition, desktopPosition);
|
|
|
|
const overlayStyle = (payload as any).overlayStyle || 'gradient';
|
|
const overlayOpacity = (payload as any).overlayOpacity !== undefined ? (payload as any).overlayOpacity : 45;
|
|
|
|
const renderOverlay = () => {
|
|
if (overlayStyle === 'none' || overlayOpacity === 0) {
|
|
return null;
|
|
}
|
|
|
|
const opacityRatio = overlayOpacity / 100;
|
|
|
|
if (overlayStyle === 'solid') {
|
|
return (
|
|
<div
|
|
className="absolute inset-0 transition-opacity duration-300"
|
|
style={{ backgroundColor: isDarkText ? `rgba(255, 255, 255, ${opacityRatio})` : `rgba(0, 0, 0, ${opacityRatio})` }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Gradient style (default): smooth vertical gradient scaled by opacity
|
|
const bottomAlpha = Math.min(0.95, opacityRatio * 1.85);
|
|
const midAlpha = opacityRatio;
|
|
const topAlpha = Math.max(0, opacityRatio * 0.65);
|
|
|
|
const rgb = isDarkText ? '255, 255, 255' : '0, 0, 0';
|
|
|
|
return (
|
|
<div
|
|
className="absolute inset-0 transition-opacity duration-300"
|
|
style={{
|
|
background: `linear-gradient(to top, rgba(${rgb}, ${bottomAlpha}) 0%, rgba(${rgb}, ${midAlpha}) 50%, rgba(${rgb}, ${topAlpha}) 100%)`,
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const containerClasses = getHeroContainerWidthClasses(contentWidth);
|
|
const titleSize = (payload as any).titleSize || 'xl';
|
|
const titleSizeClass = getHeroTitleSizeClass(titleSize);
|
|
const textWrapperWidthClass = titleSize === 'sm' || contentWidth === 'compact' ? 'max-w-xl' : titleSize === 'md' ? 'max-w-2xl' : 'max-w-3xl';
|
|
|
|
const aspectRatio = (payload as any).aspectRatio;
|
|
const mobileAspectRatio = (payload as any).mobileAspectRatio;
|
|
const mobileImageUrl = (payload as any).mobileImageUrl;
|
|
const isFullScreen = !aspectRatio || aspectRatio === 'full_screen';
|
|
|
|
const getAspectClass = () => {
|
|
switch (aspectRatio) {
|
|
case '21/6': return 'md:aspect-[21/6]';
|
|
case '21/7': return 'md:aspect-[21/7]';
|
|
case '21/9': return 'md:aspect-[21/9]';
|
|
case '16/5': return 'md:aspect-[16/5]';
|
|
case '16/6': return 'md:aspect-[16/6]';
|
|
case '4/1': return 'md:aspect-[4/1]';
|
|
case '5/1': return 'md:aspect-[5/1]';
|
|
case 'full_screen': return 'md:h-[90vh] md:aspect-[21/9]';
|
|
default: return 'md:aspect-[21/7]';
|
|
}
|
|
};
|
|
|
|
const getMobileAspectClass = () => {
|
|
switch (mobileAspectRatio) {
|
|
case '16/9': return 'aspect-[16/9]';
|
|
case '4/3': return 'aspect-[4/3]';
|
|
case '1/1': return 'aspect-[1/1]';
|
|
case '21/9': return 'aspect-[21/9]';
|
|
case 'full_screen': return 'min-h-[85vh]';
|
|
default: return 'aspect-[16/9]';
|
|
}
|
|
};
|
|
|
|
const sectionHeightClass = isFullScreen
|
|
? 'min-h-[85vh] sm:min-h-[90vh] md:h-[90vh] md:aspect-[21/9]'
|
|
: `${getMobileAspectClass()} ${getAspectClass()}`;
|
|
const innerHeightClass = isFullScreen
|
|
? 'min-h-[85vh] sm:min-h-[90vh] md:min-h-full'
|
|
: 'h-full justify-center';
|
|
|
|
return (
|
|
<section className={`relative w-full ${sectionHeightClass} flex overflow-hidden ${isDarkText ? 'bg-surface-page' : 'bg-night-950'}`}>
|
|
<div className="absolute inset-0 w-full h-full z-0">
|
|
<img
|
|
src={imageUrl || `${import.meta.env.VITE_API_URL.replace('/api', '')}/assets/hero.png`}
|
|
className={`w-full h-full object-cover ${mobileImageUrl ? 'hidden md:block' : ''}`}
|
|
alt="Hero Background"
|
|
style={{ objectPosition: focalPoint ? `${focalPoint.x} ${focalPoint.y}` : undefined }}
|
|
onError={(e) => {
|
|
(e.target as HTMLImageElement).style.display = 'none';
|
|
}}
|
|
/>
|
|
{mobileImageUrl && (
|
|
<img
|
|
src={mobileImageUrl}
|
|
className="w-full h-full object-cover block md:hidden"
|
|
alt="Hero Background Mobile"
|
|
style={{ objectPosition: focalPoint ? `${focalPoint.x} ${focalPoint.y}` : undefined }}
|
|
onError={(e) => {
|
|
(e.target as HTMLImageElement).style.display = 'none';
|
|
}}
|
|
/>
|
|
)}
|
|
{/* Dynamic Dark / Light Gradient / Solid Overlay */}
|
|
{renderOverlay()}
|
|
</div>
|
|
|
|
<div className={`relative z-10 ${innerHeightClass} flex flex-col ${containerClasses} ${wrapperClasses} animate-fade-in`}>
|
|
<div className={`w-full ${textWrapperWidthClass} flex flex-col space-y-5 ${contentClasses}`}>
|
|
{tagline && (
|
|
<p className={`${isDarkText ? 'text-pop-pink' : 'text-pop-yellow'} font-semibold tracking-[0.3em] text-xs md:text-sm uppercase drop-shadow-md`}>
|
|
{tagline}
|
|
</p>
|
|
)}
|
|
{title && (
|
|
<h1 className={`ui-hero-title ${titleSizeClass} ${isDarkText ? '!text-pop-black' : '!text-white'} drop-shadow-lg`}>
|
|
{renderTitle(title)}
|
|
</h1>
|
|
)}
|
|
{description && (
|
|
<p className={`ui-hero-copy ${isDarkText ? '!text-pop-black/85' : '!text-white/90'} drop-shadow-md !pb-2 ${contentClasses.includes('text-left') ? '!mx-0' : contentClasses.includes('text-right') ? '!ml-auto !mr-0' : '!mx-auto'}`}>
|
|
{description}
|
|
</p>
|
|
)}
|
|
<div className={`flex flex-col sm:flex-row gap-4 sm:gap-6 w-full ${actionsClasses} pt-1`}>
|
|
{buttonText && (
|
|
<Link
|
|
to={buttonLink || "/shop"}
|
|
className="ui-hero-button shadow-lg shadow-black/30"
|
|
>
|
|
{buttonText}
|
|
</Link>
|
|
)}
|
|
<a
|
|
href="#categories"
|
|
className={`group flex items-center gap-1.5 drop-shadow-md hover:underline ${isDarkText ? 'text-pop-black hover:text-pop-pink' : 'ui-dark-link'}`}
|
|
>
|
|
<span>{t('hero.learnMore')}</span>
|
|
<span className="group-hover:translate-x-1 transition-transform">→</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|