x
This commit is contained in:
1 parent
e349ef5b84
commit
8d415ecef7
14 files changed
+372
-119
No files matched your search
@@ -20,8 +20,17 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) {
|
||||
|
||||
const desktopPosition = payload.buttonPosition || 'center-left';
|
||||
const mobilePosition = payload.mobileButtonPosition || desktopPosition;
|
||||
const contentWidth = (payload as any).contentWidth || 'standard';
|
||||
const textColorScheme = (payload as any).textColorScheme || 'light';
|
||||
const isDarkText = textColorScheme === 'dark';
|
||||
const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(mobilePosition, desktopPosition);
|
||||
|
||||
const shellWidthClass = contentWidth === 'full'
|
||||
? '!max-w-none !rounded-none md:!rounded-3xl mx-4 md:mx-8'
|
||||
: contentWidth === 'compact'
|
||||
? '!max-w-4xl'
|
||||
: '!max-w-7xl';
|
||||
|
||||
const bannerInner = (
|
||||
<>
|
||||
<div className="absolute inset-0 z-0">
|
||||
@@ -31,23 +40,23 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) {
|
||||
alt={title || "Banner"}
|
||||
style={imageStyle}
|
||||
/>
|
||||
<div className="ui-banner-overlay"></div>
|
||||
<div className={isDarkText ? "absolute inset-0 bg-white/40 backdrop-blur-[1px]" : "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">
|
||||
<h3 className={`font-serif text-3xl font-light leading-tight md:text-5xl ${isDarkText ? '!text-pop-black' : 'text-pop-pink'}`}>
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
{description && (
|
||||
<p className="font-light text-base text-pop-bg/95 md:text-lg">
|
||||
<p className={`font-light text-base md:text-lg ${isDarkText ? '!text-pop-black/80' : 'text-pop-bg/95'}`}>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{buttonText && (
|
||||
<div className={`pt-4 flex ${actionsClasses}`}>
|
||||
<span className="ui-button-secondary">
|
||||
<span className={isDarkText ? "ui-button-primary" : "ui-button-secondary"}>
|
||||
{buttonText}
|
||||
</span>
|
||||
</div>
|
||||
@@ -59,11 +68,11 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) {
|
||||
return (
|
||||
<section className="ui-section-band bg-pop-bg relative overflow-hidden">
|
||||
{linkUrl ? (
|
||||
<Link to={linkUrl} className={`ui-banner-shell group flex flex-col ${wrapperClasses} cursor-pointer`}>
|
||||
<Link to={linkUrl} className={`ui-banner-shell group flex flex-col ${shellWidthClass} ${wrapperClasses} cursor-pointer`}>
|
||||
{bannerInner}
|
||||
</Link>
|
||||
) : (
|
||||
<div className={`ui-banner-shell group flex flex-col ${wrapperClasses} cursor-default`}>
|
||||
<div className={`ui-banner-shell group flex flex-col ${shellWidthClass} ${wrapperClasses} cursor-default`}>
|
||||
{bannerInner}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -139,12 +139,25 @@ export function getHeroPositionClasses(mobilePos: string, desktopPos: string) {
|
||||
};
|
||||
}
|
||||
|
||||
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 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
|
||||
@@ -167,6 +180,9 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
|
||||
? 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) => {
|
||||
@@ -174,7 +190,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
|
||||
const [accent, rest] = part.split(']');
|
||||
return (
|
||||
<span key={i}>
|
||||
<span className="text-pop-yellow font-medium italic">
|
||||
<span className={`${isDarkText ? 'text-pop-pink' : 'text-pop-yellow'} font-medium italic`}>
|
||||
{accent}
|
||||
</span>
|
||||
{rest}
|
||||
@@ -201,7 +217,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 transition-opacity duration-300"
|
||||
style={{ backgroundColor: `rgba(0, 0, 0, ${opacityRatio})` }}
|
||||
style={{ backgroundColor: isDarkText ? `rgba(255, 255, 255, ${opacityRatio})` : `rgba(0, 0, 0, ${opacityRatio})` }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -211,18 +227,22 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
|
||||
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(0, 0, 0, ${bottomAlpha}) 0%, rgba(0, 0, 0, ${midAlpha}) 50%, rgba(0, 0, 0, ${topAlpha}) 100%)`,
|
||||
background: `linear-gradient(to top, rgba(${rgb}, ${bottomAlpha}) 0%, rgba(${rgb}, ${midAlpha}) 50%, rgba(${rgb}, ${topAlpha}) 100%)`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const containerClasses = getHeroContainerWidthClasses(contentWidth);
|
||||
|
||||
return (
|
||||
<section className="relative w-full min-h-[85vh] sm:min-h-[90vh] md:h-[90vh] md:aspect-[21/9] flex bg-night-950 overflow-hidden">
|
||||
<section className={`relative w-full min-h-[85vh] sm:min-h-[90vh] md:h-[90vh] md:aspect-[21/9] 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`}
|
||||
@@ -233,24 +253,24 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{/* Dynamic Dark Gradient / Solid Overlay */}
|
||||
{/* Dynamic Dark / Light Gradient / Solid Overlay */}
|
||||
{renderOverlay()}
|
||||
</div>
|
||||
|
||||
<div className={`relative z-10 w-full min-h-[85vh] sm:min-h-[90vh] md:min-h-full flex flex-col ${wrapperClasses} animate-fade-in`}>
|
||||
<div className={`relative z-10 min-h-[85vh] sm:min-h-[90vh] md:min-h-full flex flex-col ${containerClasses} ${wrapperClasses} animate-fade-in`}>
|
||||
<div className={`w-full max-w-3xl flex flex-col space-y-5 ${contentClasses}`}>
|
||||
{tagline && (
|
||||
<p className="text-pop-yellow font-semibold tracking-[0.3em] text-xs md:text-sm uppercase drop-shadow-md">
|
||||
<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 !text-white drop-shadow-lg">
|
||||
<h1 className={`ui-hero-title ${isDarkText ? '!text-pop-black' : '!text-white'} drop-shadow-lg`}>
|
||||
{renderTitle(title)}
|
||||
</h1>
|
||||
)}
|
||||
{description && (
|
||||
<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'}`}>
|
||||
<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>
|
||||
)}
|
||||
@@ -265,7 +285,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
|
||||
)}
|
||||
<a
|
||||
href="#categories"
|
||||
className="ui-dark-link group flex items-center gap-1.5 drop-shadow-md hover:underline"
|
||||
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>
|
||||
|
||||
@@ -4,7 +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 { getHeroPositionClasses, getHeroContainerWidthClasses } from './DynamicHero';
|
||||
|
||||
// Import Swiper styles
|
||||
import 'swiper/css';
|
||||
@@ -119,28 +119,34 @@ export default function DynamicHeroCarousel({ payload }: DynamicHeroCarouselProp
|
||||
{(slide.title || slide.subtitle || slide.buttonText) && (() => {
|
||||
const slideDesktopPos = (slide as any).buttonPosition || 'bottom-left';
|
||||
const slideMobilePos = (slide as any).mobileButtonPosition || slideDesktopPos;
|
||||
const slideWidth = (slide as any).contentWidth || 'standard';
|
||||
const slideColor = (slide as any).textColorScheme || 'light';
|
||||
const isDarkSlide = slideColor === 'dark';
|
||||
const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(slideMobilePos, slideDesktopPos);
|
||||
const containerClasses = getHeroContainerWidthClasses(slideWidth);
|
||||
|
||||
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 className={`absolute inset-0 z-10 flex flex-col py-6 md:py-10 ${isDarkSlide ? 'bg-gradient-to-t from-white/80 via-white/30 to-transparent' : 'bg-gradient-to-t from-black/70 via-black/20 to-transparent'} ${wrapperClasses}`}>
|
||||
<div className={`${containerClasses} flex flex-col ${contentClasses}`}>
|
||||
<div className={`space-y-1.5 md:space-y-2 flex flex-col ${contentClasses} ${slideWidth === 'full' ? 'max-w-2xl' : 'max-w-xl'}`}>
|
||||
{slide.subtitle && (
|
||||
<p className={`text-[10px] md:text-xs uppercase font-bold tracking-widest drop-shadow-md ${isDarkSlide ? 'text-pop-pink' : 'text-pop-yellow'}`}>
|
||||
{slide.subtitle}
|
||||
</p>
|
||||
)}
|
||||
{slide.title && (
|
||||
<h3 className={`text-lg md:text-3xl font-bold drop-shadow-md leading-tight ${isDarkSlide ? 'text-pop-black' : 'text-white'}`}>
|
||||
{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>
|
||||
);
|
||||
|
||||
@@ -2,7 +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';
|
||||
import { getHeroPositionClasses, getHeroContainerWidthClasses } from './DynamicHero';
|
||||
|
||||
interface DynamicHeroVideoProps {
|
||||
payload: HeroVideoSectionPayload;
|
||||
@@ -92,60 +92,73 @@ export default function DynamicHeroVideo({ payload }: DynamicHeroVideoProps) {
|
||||
) : null}
|
||||
|
||||
{/* Customizable Overlay Opacity */}
|
||||
<div
|
||||
className="absolute inset-0 z-10 bg-black transition-opacity"
|
||||
style={{ opacity: overlayOpacity / 100 }}
|
||||
></div>
|
||||
|
||||
{/* Overlay Typography Content */}
|
||||
{(() => {
|
||||
const textColorScheme = (payload as any).textColorScheme || 'light';
|
||||
const isDarkText = textColorScheme === 'dark';
|
||||
const desktopPosition = (payload as any).buttonPosition || 'center';
|
||||
const mobilePosition = (payload as any).mobileButtonPosition || desktopPosition;
|
||||
const contentWidth = (payload as any).contentWidth || 'standard';
|
||||
const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(mobilePosition, desktopPosition);
|
||||
const containerClasses = getHeroContainerWidthClasses(contentWidth);
|
||||
|
||||
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>
|
||||
)}
|
||||
<>
|
||||
<div
|
||||
className={`absolute inset-0 z-10 transition-opacity ${isDarkText ? 'bg-white' : 'bg-black'}`}
|
||||
style={{ opacity: overlayOpacity / 100 }}
|
||||
></div>
|
||||
|
||||
<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>
|
||||
<div className={`relative z-20 py-16 h-full flex flex-col animate-fade-in ${containerClasses} ${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 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 ${isDarkText ? 'text-pop-black' : 'text-white'}`}>
|
||||
{renderTitle(title) || renderTitle(t('hero.defaultTitle'))}
|
||||
</h1>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
{description && (
|
||||
<p className={`max-w-2xl text-sm md:text-lg font-light leading-relaxed drop-shadow ${isDarkText ? 'text-pop-black/80' : 'text-white/90'}`}>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 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 font-medium text-xs uppercase tracking-widest transition-all shadow-lg rounded-sm ${
|
||||
isDarkText
|
||||
? 'bg-neutral-900 text-white hover:bg-pop-pink'
|
||||
: 'bg-white text-neutral-900 hover:bg-pop-pink hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{buttonText}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{secondaryButtonText && payload.secondaryButtonLink && (
|
||||
<Link
|
||||
to={payload.secondaryButtonLink}
|
||||
className={`px-8 py-3.5 bg-transparent border font-medium text-xs uppercase tracking-widest transition-all rounded-sm backdrop-blur-sm ${
|
||||
isDarkText
|
||||
? 'border-neutral-900/40 text-neutral-900 hover:bg-neutral-900/10 hover:border-neutral-900'
|
||||
: 'border-white/40 text-white hover:bg-white/10 hover:border-white'
|
||||
}`}
|
||||
>
|
||||
{secondaryButtonText}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</section>
|
||||
|
||||
Reference in new issue
Block a user