This commit is contained in:
Your Name committed 2026-10-04 18:59:27 +07:00
1 parent b641acbeb7
commit e349ef5b84
14 files changed
+720 -558

No files matched your search

+43 -60
View File
@@ -2,6 +2,7 @@ import { Link } from 'react-router-dom';
import type { BannerSectionPayload } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
import { getHeroPositionClasses } from './DynamicHero';
interface DynamicBannerProps {
payload: BannerSectionPayload;
@@ -17,71 +18,53 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) {
const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? payload.focalPoint : undefined;
const imageStyle = focalPoint ? { objectPosition: `${focalPoint.x} ${focalPoint.y}` } : undefined;
const desktopPosition = payload.buttonPosition || 'center-left';
const mobilePosition = payload.mobileButtonPosition || desktopPosition;
const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(mobilePosition, desktopPosition);
const bannerInner = (
<>
<div className="absolute inset-0 z-0">
<img
src={imageUrl}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-[1.03]"
alt={title || "Banner"}
style={imageStyle}
/>
<div className="ui-banner-overlay"></div>
</div>
<div className={`ui-banner-content flex flex-col ${contentClasses}`}>
{title && (
<h3 className="font-serif text-3xl font-light leading-tight text-pop-pink md:text-5xl">
{title}
</h3>
)}
{description && (
<p className="font-light text-base text-pop-bg/95 md:text-lg">
{description}
</p>
)}
{buttonText && (
<div className={`pt-4 flex ${actionsClasses}`}>
<span className="ui-button-secondary">
{buttonText}
</span>
</div>
)}
</div>
</>
);
return (
<section className="ui-section-band bg-pop-bg relative overflow-hidden">
{linkUrl ? (
<Link to={linkUrl} className="ui-banner-shell group block cursor-pointer">
<div className="absolute inset-0 z-0">
<img
src={imageUrl}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-[1.03]"
alt={title || "Banner"}
style={imageStyle}
/>
<div className="ui-banner-overlay"></div>
</div>
<div className="ui-banner-content">
{title && (
<h3 className="font-serif text-3xl font-light leading-tight text-pop-pink md:text-5xl">
{title}
</h3>
)}
{description && (
<p className="font-light text-base text-pop-bg/95 md:text-lg">
{description}
</p>
)}
{buttonText && (
<div className="pt-4">
<span className="ui-button-secondary">
{buttonText}
</span>
</div>
)}
</div>
<Link to={linkUrl} className={`ui-banner-shell group flex flex-col ${wrapperClasses} cursor-pointer`}>
{bannerInner}
</Link>
) : (
<div className="ui-banner-shell group block cursor-default">
<div className="absolute inset-0 z-0">
<img
src={imageUrl}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-[1.03]"
alt={title || "Banner"}
style={imageStyle}
/>
<div className="ui-banner-overlay"></div>
</div>
<div className="ui-banner-content">
{title && (
<h3 className="font-serif text-3xl font-light leading-tight text-pop-pink md:text-5xl">
{title}
</h3>
)}
{description && (
<p className="font-light text-base text-pop-bg/95 md:text-lg">
{description}
</p>
)}
{buttonText && (
<div className="pt-4">
<span className="ui-button-secondary">
{buttonText}
</span>
</div>
)}
</div>
<div className={`ui-banner-shell group flex flex-col ${wrapperClasses} cursor-default`}>
{bannerInner}
</div>
)}
</section>
+134 -129
View File
@@ -7,6 +7,138 @@ 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 default function DynamicHero({ payload }: DynamicHeroProps) {
const { t, i18n } = useTranslation();
const lang = i18n.language;
@@ -53,134 +185,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
});
};
// Map 9-box grid position to mobile flex classes
const getMobilePositionClasses = (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
const getDesktopPositionClasses = (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'
};
}
};
const mClasses = getMobilePositionClasses(mobilePosition);
const dClasses = getDesktopPositionClasses(desktopPosition);
const wrapperClasses = `${mClasses.wrapper} ${dClasses.wrapper}`;
const contentClasses = `${mClasses.content} ${dClasses.content}`;
const actionsClasses = `${mClasses.actions} ${dClasses.actions}`;
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;
@@ -245,7 +250,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
</h1>
)}
{description && (
<p className="ui-hero-copy !text-white/90 drop-shadow-md !pb-2">
<p className={`ui-hero-copy !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>
)}
+29 -20
View File
@@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
import type { HeroCarouselSectionPayload } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
import { getHeroPositionClasses } from './DynamicHero';
// Import Swiper styles
import 'swiper/css';
@@ -115,27 +116,35 @@ export default function DynamicHeroCarousel({ payload }: DynamicHeroCarouselProp
/>
{/* Text Overlay */}
{(slide.title || slide.subtitle || slide.buttonText) && (
<div className="absolute inset-0 z-10 bg-gradient-to-t from-black/70 via-black/20 to-transparent flex flex-col justify-end p-6 md:p-10">
<div className="max-w-xl space-y-1.5 md:space-y-2">
{slide.subtitle && (
<p className="text-[10px] md:text-xs uppercase font-bold tracking-widest text-pop-yellow drop-shadow-md">
{slide.subtitle}
</p>
)}
{slide.title && (
<h3 className="text-lg md:text-3xl font-bold text-white drop-shadow-md leading-tight">
{renderTitle(slide.title)}
</h3>
)}
{slide.buttonText && (
<span className="inline-block mt-2 px-4 py-1.5 bg-pop-pink text-white text-xs font-bold rounded-full shadow-lg hover:bg-pop-red transition-all">
{slide.buttonText}
</span>
)}
{(slide.title || slide.subtitle || slide.buttonText) && (() => {
const slideDesktopPos = (slide as any).buttonPosition || 'bottom-left';
const slideMobilePos = (slide as any).mobileButtonPosition || slideDesktopPos;
const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(slideMobilePos, slideDesktopPos);
return (
<div className={`absolute inset-0 z-10 bg-gradient-to-t from-black/70 via-black/20 to-transparent flex flex-col p-6 md:p-10 ${wrapperClasses}`}>
<div className={`max-w-xl space-y-1.5 md:space-y-2 flex flex-col ${contentClasses}`}>
{slide.subtitle && (
<p className="text-[10px] md:text-xs uppercase font-bold tracking-widest text-pop-yellow drop-shadow-md">
{slide.subtitle}
</p>
)}
{slide.title && (
<h3 className="text-lg md:text-3xl font-bold text-white drop-shadow-md leading-tight">
{renderTitle(slide.title)}
</h3>
)}
{slide.buttonText && (
<div className={`flex ${actionsClasses} pt-1`}>
<span className="inline-block mt-2 px-4 py-1.5 bg-pop-pink text-white text-xs font-bold rounded-full shadow-lg hover:bg-pop-red transition-all">
{slide.buttonText}
</span>
</div>
)}
</div>
</div>
</div>
)}
);
})()}
</div>
);
+46 -35
View File
@@ -2,6 +2,7 @@ import { Link } from 'react-router-dom';
import type { HeroVideoSectionPayload } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
import { getHeroPositionClasses } from './DynamicHero';
interface DynamicHeroVideoProps {
payload: HeroVideoSectionPayload;
@@ -97,46 +98,56 @@ export default function DynamicHeroVideo({ payload }: DynamicHeroVideoProps) {
></div>
{/* Overlay Typography Content */}
<div className="relative z-20 mx-auto px-6 py-16 w-full max-w-5xl text-center space-y-6 flex flex-col items-center justify-center animate-fade-in">
{tagline && (
<p className="text-xs md:text-sm font-semibold uppercase tracking-[0.35em] text-pop-pink/90 drop-shadow-md">
{tagline}
</p>
)}
{(() => {
const desktopPosition = (payload as any).buttonPosition || 'center';
const mobilePosition = (payload as any).mobileButtonPosition || desktopPosition;
const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(mobilePosition, desktopPosition);
<h1 className="font-serif text-4xl md:text-6xl lg:text-7xl font-light leading-tight tracking-wide drop-shadow-lg text-white">
{renderTitle(title) || renderTitle(t('hero.defaultTitle'))}
</h1>
return (
<div className={`relative z-20 mx-auto px-6 py-16 w-full max-w-5xl h-full flex flex-col animate-fade-in ${wrapperClasses}`}>
<div className={`flex flex-col space-y-6 max-w-3xl ${contentClasses}`}>
{tagline && (
<p className="text-xs md:text-sm font-semibold uppercase tracking-[0.35em] text-pop-pink/90 drop-shadow-md">
{tagline}
</p>
)}
{description && (
<p className="max-w-2xl text-sm md:text-lg font-light leading-relaxed opacity-90 drop-shadow">
{description}
</p>
)}
<h1 className="font-serif text-4xl md:text-6xl lg:text-7xl font-light leading-tight tracking-wide drop-shadow-lg text-white">
{renderTitle(title) || renderTitle(t('hero.defaultTitle'))}
</h1>
{/* Dual Action Buttons */}
{(buttonText || secondaryButtonText) && (
<div className="flex flex-wrap items-center justify-center gap-4 pt-4">
{buttonText && payload.buttonLink && (
<Link
to={payload.buttonLink}
className="px-8 py-3.5 bg-white text-neutral-900 font-medium text-xs uppercase tracking-widest hover:bg-pop-pink hover:text-white transition-all shadow-lg rounded-sm"
>
{buttonText}
</Link>
)}
{description && (
<p className="max-w-2xl text-sm md:text-lg font-light leading-relaxed opacity-90 drop-shadow">
{description}
</p>
)}
{secondaryButtonText && payload.secondaryButtonLink && (
<Link
to={payload.secondaryButtonLink}
className="px-8 py-3.5 bg-transparent border border-white/40 text-white font-medium text-xs uppercase tracking-widest hover:bg-white/10 hover:border-white transition-all rounded-sm backdrop-blur-sm"
>
{secondaryButtonText}
</Link>
)}
{/* Dual Action Buttons */}
{(buttonText || secondaryButtonText) && (
<div className={`flex flex-wrap gap-4 pt-4 ${actionsClasses}`}>
{buttonText && payload.buttonLink && (
<Link
to={payload.buttonLink}
className="px-8 py-3.5 bg-white text-neutral-900 font-medium text-xs uppercase tracking-widest hover:bg-pop-pink hover:text-white transition-all shadow-lg rounded-sm"
>
{buttonText}
</Link>
)}
{secondaryButtonText && payload.secondaryButtonLink && (
<Link
to={payload.secondaryButtonLink}
className="px-8 py-3.5 bg-transparent border border-white/40 text-white font-medium text-xs uppercase tracking-widest hover:bg-white/10 hover:border-white transition-all rounded-sm backdrop-blur-sm"
>
{secondaryButtonText}
</Link>
)}
</div>
)}
</div>
</div>
)}
</div>
);
})()}
</section>
);
}