x
This commit is contained in:
1 parent
862eeef48b
commit
5c09629b6f
12 files changed
+135
-47
No files matched your search
@@ -0,0 +1,52 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
|
||||||
|
interface FocalPointPickerProps {
|
||||||
|
imageUrl: string;
|
||||||
|
focalPoint?: { x: string; y: string };
|
||||||
|
onChange: (focalPoint: { x: string; y: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FocalPointPicker({ imageUrl, focalPoint, onChange }: FocalPointPickerProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||||
|
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||||
|
onChange({ x: `${x.toFixed(2)}%`, y: `${y.toFixed(2)}%` });
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentX = focalPoint?.x || '50%';
|
||||||
|
const currentY = focalPoint?.y || '50%';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">
|
||||||
|
Focal Point (Click image to set)
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
onClick={handleClick}
|
||||||
|
className="relative w-full overflow-hidden rounded-xl border border-white/10 bg-night-950/50 cursor-crosshair"
|
||||||
|
style={{ aspectRatio: '16/9' }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt="Focal point"
|
||||||
|
className="absolute inset-0 w-full h-full object-cover"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute w-6 h-6 -ml-3 -mt-3 bg-white/20 border-2 border-white rounded-full shadow-[0_0_10px_rgba(0,0,0,0.5)] pointer-events-none flex items-center justify-center backdrop-blur-sm"
|
||||||
|
style={{ left: currentX, top: currentY }}
|
||||||
|
>
|
||||||
|
<div className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-400 font-mono">
|
||||||
|
X: {currentX}, Y: {currentY}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { UploadCloud, Image as ImageIcon } from 'lucide-react';
|
import { UploadCloud, Image as ImageIcon } from 'lucide-react';
|
||||||
import { type PageSection } from '@homaroma/types';
|
import { type PageSection } from '@homaroma/types';
|
||||||
|
import FocalPointPicker from './FocalPointPicker';
|
||||||
|
|
||||||
interface HeroFormProps {
|
interface HeroFormProps {
|
||||||
editingSection: Partial<PageSection>;
|
editingSection: Partial<PageSection>;
|
||||||
editingLang: string;
|
editingLang: string;
|
||||||
getLocalizedValue: (payload: any, field: string) => string;
|
getLocalizedValue: (payload: any, field: string) => string;
|
||||||
updateLocalizedValue: (field: string, value: string) => void;
|
updateLocalizedValue: (field: string, value: string) => void;
|
||||||
updatePayloadValue: (field: string, value: string | number) => void;
|
updatePayloadValue: (field: string, value: any) => void;
|
||||||
setUploadTarget: (target: { type: 'main' | 'slide'; index?: number } | null) => void;
|
setUploadTarget: (target: { type: 'main' | 'slide'; index?: number } | null) => void;
|
||||||
setIsMediaModalOpen: (open: boolean) => void;
|
setIsMediaModalOpen: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
@@ -47,6 +48,16 @@ export default function HeroForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{editingSection.payload?.imageUrl && (
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<FocalPointPicker
|
||||||
|
imageUrl={editingSection.payload.imageUrl}
|
||||||
|
focalPoint={editingSection.payload.focalPoint}
|
||||||
|
onChange={(focalPoint) => updatePayloadValue('focalPoint', focalPoint)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Tagline</label>
|
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Tagline</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
|
|||||||
const title = getLocalizedField(payload.title, lang);
|
const title = getLocalizedField(payload.title, lang);
|
||||||
const description = getLocalizedField(payload.description, lang);
|
const description = getLocalizedField(payload.description, lang);
|
||||||
const buttonText = getLocalizedField(payload.buttonText, lang);
|
const buttonText = getLocalizedField(payload.buttonText, lang);
|
||||||
|
const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined;
|
||||||
|
|
||||||
const renderTitle = (text: string) => {
|
const renderTitle = (text: string) => {
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
@@ -41,6 +42,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
|
|||||||
src={imageUrl || `${import.meta.env.VITE_API_URL.replace('/api', '')}/assets/hero.png`}
|
src={imageUrl || `${import.meta.env.VITE_API_URL.replace('/api', '')}/assets/hero.png`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
alt="Hero Background"
|
alt="Hero Background"
|
||||||
|
style={{ objectPosition: focalPoint ? `${focalPoint.x} ${focalPoint.y}` : undefined }}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).style.display = 'none';
|
(e.target as HTMLImageElement).style.display = 'none';
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default function DynamicHeroV2({ payload }: DynamicHeroV2Props) {
|
|||||||
const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/assets/hero.png';
|
const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/assets/hero.png';
|
||||||
const align = typeof payload.contentAlignment === 'string' ? payload.contentAlignment : 'left';
|
const align = typeof payload.contentAlignment === 'string' ? payload.contentAlignment : 'left';
|
||||||
const showOrnament = typeof payload.showOrnament === 'boolean' ? payload.showOrnament : true;
|
const showOrnament = typeof payload.showOrnament === 'boolean' ? payload.showOrnament : true;
|
||||||
|
const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined;
|
||||||
|
|
||||||
const alignClass = align === 'center' ? 'text-center items-center mx-auto' : align === 'right' ? 'text-right items-end ml-auto' : 'text-left items-start';
|
const alignClass = align === 'center' ? 'text-center items-center mx-auto' : align === 'right' ? 'text-right items-end ml-auto' : 'text-left items-start';
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ export default function DynamicHeroV2({ payload }: DynamicHeroV2Props) {
|
|||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={title || 'Hero'}
|
alt={title || 'Hero'}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
|
style={{ objectPosition: focalPoint ? `${focalPoint.x} ${focalPoint.y}` : undefined }}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-white/90 via-white/50 to-transparent pointer-events-none" />
|
<div className="absolute inset-0 bg-gradient-to-r from-white/90 via-white/50 to-transparent pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ import { useMenuStore } from '../store/menuStore'
|
|||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
categories?: Category[]
|
categories?: Category[]
|
||||||
topOffsetClassName?: string
|
topOffsetClassName?: string
|
||||||
|
isTransparent?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = false }: NavbarProps) {
|
||||||
const isDesktop = useMediaQuery('(min-width: 768px)')
|
const isDesktop = useMediaQuery('(min-width: 768px)')
|
||||||
|
|
||||||
const [searchOpen, setSearchOpen] = useState(false)
|
const [searchOpen, setSearchOpen] = useState(false)
|
||||||
@@ -147,12 +148,18 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
|||||||
|
|
||||||
const logoUrl = settings?.logoUrl || ''
|
const logoUrl = settings?.logoUrl || ''
|
||||||
|
|
||||||
|
const shouldBeTransparent = isTransparent && !scrolled && !mobileOpen && !searchOpen && !langSheetOpen
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav
|
<nav
|
||||||
ref={navRef}
|
ref={navRef}
|
||||||
className={`fixed ${finalTopClassName} z-50 w-full border-b border-pop-black/10 transition-all duration-300 ${
|
className={`fixed ${finalTopClassName} z-50 w-full transition-all duration-300 ${
|
||||||
scrolled ? 'shadow-lg shadow-pop-black/10 backdrop-blur-xl bg-pop-bg/90' : 'shadow-none bg-pop-bg'
|
scrolled
|
||||||
|
? 'border-b border-pop-black/10 shadow-lg shadow-pop-black/10 backdrop-blur-xl bg-pop-bg/90'
|
||||||
|
: shouldBeTransparent
|
||||||
|
? 'border-transparent shadow-none bg-transparent backdrop-blur-none'
|
||||||
|
: 'border-b border-pop-black/10 shadow-none bg-pop-bg'
|
||||||
} px-4 py-3.5 md:px-6 md:py-4`}
|
} px-4 py-3.5 md:px-6 md:py-4`}
|
||||||
>
|
>
|
||||||
{/* Single-row layout: Logo (left) | Menu (center, desktop) | Actions (right) */}
|
{/* Single-row layout: Logo (left) | Menu (center, desktop) | Actions (right) */}
|
||||||
|
|||||||
@@ -119,42 +119,42 @@ export default function CategoryPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative h-64 md:h-80 overflow-hidden">
|
<div className="bg-spa-stone/5 border-b border-spa-stone/10 pt-32 pb-16 px-6">
|
||||||
<img
|
<div className="max-w-7xl mx-auto flex flex-col-reverse md:flex-row items-center gap-10 md:gap-16">
|
||||||
src={getCategoryImage(typeof category?.name === 'string' ? category.name : (category?.name as any)?.en || '')}
|
<div className="flex-1 text-center md:text-left">
|
||||||
className="absolute inset-0 w-full h-full object-cover brightness-30 scale-105"
|
|
||||||
alt={typeof category?.name === 'string' ? category.name : (category?.name as any)?.[i18n.language] || (category?.name as any)?.en || ''}
|
|
||||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-night-900/40" />
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-night-900/90 via-night-900/10 to-transparent" />
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 px-6 pb-10 pt-20">
|
|
||||||
<div className="max-w-7xl mx-auto">
|
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
<div className="flex items-center gap-2 text-xs text-spa-sage/80 uppercase tracking-widest mb-4 font-semibold">
|
<div className="flex items-center justify-center md:justify-start gap-2 text-xs text-spa-stone/60 uppercase tracking-widest mb-4 font-semibold">
|
||||||
<Link to="/" className="hover:text-spa-light transition-colors">Home</Link>
|
<Link to="/" className="hover:text-spa-forest transition-colors">Home</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<Link to="/shop" className="hover:text-spa-light transition-colors">Shop</Link>
|
<Link to="/shop" className="hover:text-spa-forest transition-colors">Shop</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<span className="text-spa-light">{typeof category?.name === 'string' ? category.name : (category?.name as any)?.[localStorage.getItem('app_lang') || 'en'] || (category?.name as any)?.en || '...'}</span>
|
<span className="text-spa-forest">{typeof category?.name === 'string' ? category.name : (category?.name as any)?.[localStorage.getItem('app_lang') || 'en'] || (category?.name as any)?.en || '...'}</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl md:text-5xl font-bold text-spa-light">
|
<h1 className="text-4xl md:text-5xl font-bold text-spa-forest">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="h-10 w-48 bg-spa-light animate-pulse rounded-lg" />
|
<div className="h-10 w-48 bg-spa-stone/20 animate-pulse rounded-lg mx-auto md:mx-0" />
|
||||||
) : (
|
) : (
|
||||||
typeof category?.name === 'string' ? category.name : (category?.name as any)?.[i18n.language] || (category?.name as any)?.en || ''
|
typeof category?.name === 'string' ? category.name : (category?.name as any)?.[i18n.language] || (category?.name as any)?.en || ''
|
||||||
)}
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
{!loading && category?._count && (
|
{!loading && category?._count && (
|
||||||
<p className="text-spa-sage mt-2">
|
<p className="text-spa-stone mt-4">
|
||||||
{category._count.products} products available
|
{category._count.products} products available
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="relative w-48 h-48 md:w-72 md:h-72 shrink-0 rounded-3xl overflow-hidden shadow-lg border border-spa-stone/10">
|
||||||
|
<img
|
||||||
|
src={getCategoryImage(typeof category?.name === 'string' ? category.name : (category?.name as any)?.en || '')}
|
||||||
|
className="absolute inset-0 w-full h-full object-cover"
|
||||||
|
alt={typeof category?.name === 'string' ? category.name : (category?.name as any)?.[i18n.language] || (category?.name as any)?.en || ''}
|
||||||
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Products Grid */}
|
{/* Products Grid */}
|
||||||
<div className="max-w-7xl mx-auto px-6 py-16 space-y-10">
|
<div className="max-w-7xl mx-auto px-6 py-16 space-y-10">
|
||||||
{/* Other Category Pills */}
|
{/* Other Category Pills */}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-pop-bg">
|
<div className="min-h-screen bg-pop-bg">
|
||||||
<Navbar categories={categories} />
|
<Navbar categories={categories} isTransparent={true} />
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="min-h-[60vh] flex items-center justify-center">
|
<div className="min-h-[60vh] flex items-center justify-center">
|
||||||
|
|||||||
@@ -84,25 +84,28 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
|
|||||||
{heroSections.length > 0 ? (
|
{heroSections.length > 0 ? (
|
||||||
<DynamicPageRenderer sections={heroSections} categories={categories} lang={lang} />
|
<DynamicPageRenderer sections={heroSections} categories={categories} lang={lang} />
|
||||||
) : (
|
) : (
|
||||||
<section className="relative overflow-hidden bg-surface-card">
|
<section className="relative overflow-hidden bg-surface-card border-b border-line-primary/10">
|
||||||
<Image
|
<div className="relative mx-auto max-w-7xl px-6 pt-32 pb-12 md:pt-32 md:pb-20 flex flex-col-reverse md:flex-row items-center gap-10 md:gap-16">
|
||||||
src={category.imageUrl || getCategoryImage(categoryName)}
|
<div className="flex-1 text-center md:text-left">
|
||||||
alt={categoryName}
|
<div className="mb-5 flex items-center justify-center md:justify-start gap-2 text-xs font-semibold uppercase tracking-widest text-muted-text">
|
||||||
fill
|
<Link href="/" className="transition hover:text-brand-primary">Home</Link>
|
||||||
unoptimized
|
<span>/</span>
|
||||||
className="absolute inset-0 h-full w-full object-cover"
|
<Link href="/shop" className="transition hover:text-brand-primary">Shop</Link>
|
||||||
/>
|
<span>/</span>
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-surface-page/95 via-surface-page/70 to-white/20" />
|
<span className="text-text-main">{categoryName}</span>
|
||||||
<div className="relative mx-auto max-w-7xl px-6 py-20">
|
</div>
|
||||||
<div className="mb-5 flex items-center gap-2 text-xs font-semibold uppercase tracking-widest text-muted-text">
|
<h1 className="text-4xl font-semibold text-text-main md:text-6xl">{categoryName}</h1>
|
||||||
<Link href="/" className="transition hover:text-brand-primary">Home</Link>
|
{category._count && <p className="mt-4 text-muted-text">{category._count.products} products available</p>}
|
||||||
<span>/</span>
|
</div>
|
||||||
<Link href="/shop" className="transition hover:text-brand-primary">Shop</Link>
|
<div className="relative w-48 h-48 md:w-72 md:h-72 shrink-0 rounded-3xl overflow-hidden shadow-lg border border-line-primary/5">
|
||||||
<span>/</span>
|
<Image
|
||||||
<span className="text-text-main">{categoryName}</span>
|
src={category.imageUrl || getCategoryImage(categoryName)}
|
||||||
|
alt={categoryName}
|
||||||
|
fill
|
||||||
|
unoptimized
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-5xl font-semibold text-text-main md:text-6xl">{categoryName}</h1>
|
|
||||||
{category._count && <p className="mt-3 text-muted-text">{category._count.products} products available</p>}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default async function ShopPage({ searchParams, params }: { searchParams:
|
|||||||
{heroSections.length > 0 ? (
|
{heroSections.length > 0 ? (
|
||||||
<DynamicPageRenderer sections={heroSections} categories={categories} lang={lang} />
|
<DynamicPageRenderer sections={heroSections} categories={categories} lang={lang} />
|
||||||
) : (
|
) : (
|
||||||
<section className="border-b border-line-primary bg-surface-card px-6 py-16 text-center">
|
<section className="border-b border-line-primary bg-surface-card px-6 pt-32 pb-16 text-center">
|
||||||
<p className="ui-eyebrow">All Products</p>
|
<p className="ui-eyebrow">All Products</p>
|
||||||
<h1 className="mt-4 text-5xl font-semibold tracking-tight text-text-main">The Collection</h1>
|
<h1 className="mt-4 text-5xl font-semibold tracking-tight text-text-main">The Collection</h1>
|
||||||
<p className="mx-auto mt-4 max-w-2xl text-muted-text">
|
<p className="mx-auto mt-4 max-w-2xl text-muted-text">
|
||||||
|
|||||||
@@ -150,15 +150,20 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
|
|||||||
const activeLanguage = LANGUAGES.find((language) => lang.startsWith(language.code) || language.code.startsWith(lang)) ?? LANGUAGES[0]
|
const activeLanguage = LANGUAGES.find((language) => lang.startsWith(language.code) || language.code.startsWith(lang)) ?? LANGUAGES[0]
|
||||||
const brandName = settings?.brandName || ''
|
const brandName = settings?.brandName || ''
|
||||||
|
|
||||||
|
const isHome = getPathWithoutLocale(pathname) === '/'
|
||||||
|
const isTransparent = isHome && !scrolled && !mobileOpen && !searchOpen && !langSheetOpen
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav
|
<nav
|
||||||
ref={navRef}
|
ref={navRef}
|
||||||
data-scrolled={scrolled}
|
data-scrolled={scrolled}
|
||||||
className={`global-navbar fixed top-0 z-50 w-full border-b border-black/5 px-4 py-3.5 transition-all duration-300 md:px-6 md:py-4 ${
|
className={`global-navbar fixed top-0 z-50 w-full border-b px-4 py-3.5 transition-all duration-300 md:px-6 md:py-4 ${
|
||||||
scrolled
|
scrolled
|
||||||
? 'bg-surface-page/95 shadow-lg shadow-black/5 backdrop-blur-xl'
|
? 'border-black/5 bg-surface-page/95 shadow-lg shadow-black/5 backdrop-blur-xl'
|
||||||
: 'bg-surface-page/80 backdrop-blur-xl md:bg-surface-nav-rest md:backdrop-blur-none'
|
: isTransparent
|
||||||
|
? 'border-transparent bg-transparent shadow-none backdrop-blur-none'
|
||||||
|
: 'border-black/5 bg-surface-page/80 backdrop-blur-xl md:bg-surface-nav-rest md:backdrop-blur-none'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between gap-4">
|
<div className="flex w-full items-center justify-between gap-4">
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
|
|||||||
const buttonText = localizedFromRecord(payload, 'buttonText', resolvedLang)
|
const buttonText = localizedFromRecord(payload, 'buttonText', resolvedLang)
|
||||||
const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/images/hero.png'
|
const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/images/hero.png'
|
||||||
const buttonLink = typeof payload.buttonLink === 'string' ? payload.buttonLink : '/shop'
|
const buttonLink = typeof payload.buttonLink === 'string' ? payload.buttonLink : '/shop'
|
||||||
|
const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="ui-hero-shell">
|
<section className="ui-hero-shell">
|
||||||
@@ -113,6 +114,7 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
|
|||||||
fill
|
fill
|
||||||
unoptimized
|
unoptimized
|
||||||
className="absolute inset-0 h-full w-full scale-[1.02] object-cover"
|
className="absolute inset-0 h-full w-full scale-[1.02] object-cover"
|
||||||
|
style={{ objectPosition: focalPoint ? `${focalPoint.x} ${focalPoint.y}` : undefined }}
|
||||||
/>
|
/>
|
||||||
<div className="relative z-10 mx-auto mt-16 w-full max-w-4xl space-y-6 px-8 text-center md:mt-0">
|
<div className="relative z-10 mx-auto mt-16 w-full max-w-4xl space-y-6 px-8 text-center md:mt-0">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -213,6 +215,7 @@ function HeroV2Section({ payload, lang }: { payload: Record<string, unknown>, la
|
|||||||
const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/images/hero.png'
|
const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/images/hero.png'
|
||||||
const align = typeof payload.contentAlignment === 'string' ? payload.contentAlignment : 'left'
|
const align = typeof payload.contentAlignment === 'string' ? payload.contentAlignment : 'left'
|
||||||
const showOrnament = typeof payload.showOrnament === 'boolean' ? payload.showOrnament : true
|
const showOrnament = typeof payload.showOrnament === 'boolean' ? payload.showOrnament : true
|
||||||
|
const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined
|
||||||
|
|
||||||
// Alignment classes
|
// Alignment classes
|
||||||
const alignClass = align === 'center' ? 'text-center items-center mx-auto' : align === 'right' ? 'text-right items-end ml-auto' : 'text-left items-start'
|
const alignClass = align === 'center' ? 'text-center items-center mx-auto' : align === 'right' ? 'text-right items-end ml-auto' : 'text-left items-start'
|
||||||
@@ -226,6 +229,7 @@ function HeroV2Section({ payload, lang }: { payload: Record<string, unknown>, la
|
|||||||
fill
|
fill
|
||||||
unoptimized
|
unoptimized
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
|
style={{ objectPosition: focalPoint ? `${focalPoint.x} ${focalPoint.y}` : undefined }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -668,12 +672,13 @@ function HeroArticleSection({ payload, lang }: { payload: Record<string, unknown
|
|||||||
: typeof payload.publishDate === 'string'
|
: typeof payload.publishDate === 'string'
|
||||||
? payload.publishDate
|
? payload.publishDate
|
||||||
: ''
|
: ''
|
||||||
|
const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="ui-hero-shell min-h-[60vh] px-6 py-24 text-center md:min-h-[76vh]">
|
<section className="ui-hero-shell min-h-[60vh] px-6 py-24 text-center md:min-h-[76vh]">
|
||||||
{imageUrl && (
|
{imageUrl && (
|
||||||
<>
|
<>
|
||||||
<Image src={imageUrl} alt={title} fill unoptimized className="absolute inset-0 h-full w-full object-cover" />
|
<Image src={imageUrl} alt={title} fill unoptimized className="absolute inset-0 h-full w-full object-cover" style={{ objectPosition: focalPoint ? `${focalPoint.x} ${focalPoint.y}` : undefined }} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="relative z-10 mx-auto max-w-4xl space-y-6">
|
<div className="relative z-10 mx-auto max-w-4xl space-y-6">
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ export const HeroSectionPayloadSchema = z.object({
|
|||||||
imageUrl: z.string().min(1),
|
imageUrl: z.string().min(1),
|
||||||
buttonText: LocalizedStringSchema,
|
buttonText: LocalizedStringSchema,
|
||||||
buttonLink: z.string(),
|
buttonLink: z.string(),
|
||||||
|
focalPoint: z.object({ x: z.string(), y: z.string() }).optional(),
|
||||||
});
|
});
|
||||||
export type HeroSectionPayload = z.infer<typeof HeroSectionPayloadSchema>;
|
export type HeroSectionPayload = z.infer<typeof HeroSectionPayloadSchema>;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user