x
This commit is contained in:
1 parent
fc252e46cc
commit
ad637fd8b4
27 files changed
+764
-25
No files matched your search
@@ -0,0 +1,110 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getLocalizedField } from '../utils/localization';
|
||||
import { Sparkles, ChevronRight } from 'lucide-react';
|
||||
|
||||
interface DynamicHeroV2Props {
|
||||
payload: Record<string, any>;
|
||||
}
|
||||
|
||||
export default function DynamicHeroV2({ payload }: DynamicHeroV2Props) {
|
||||
const { i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
|
||||
const tagline = getLocalizedField(payload.tagline, lang);
|
||||
const title = getLocalizedField(payload.title, lang);
|
||||
const description = getLocalizedField(payload.description, lang);
|
||||
|
||||
const primaryBtnText = getLocalizedField(payload.primaryButtonText, lang);
|
||||
const primaryBtnLink = typeof payload.primaryButtonLink === 'string' ? payload.primaryButtonLink : undefined;
|
||||
const secondaryBtnText = getLocalizedField(payload.secondaryButtonText, lang);
|
||||
const secondaryBtnLink = typeof payload.secondaryButtonLink === 'string' ? payload.secondaryButtonLink : undefined;
|
||||
|
||||
const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/assets/hero.png';
|
||||
const align = typeof payload.contentAlignment === 'string' ? payload.contentAlignment : 'left';
|
||||
const showOrnament = typeof payload.showOrnament === 'boolean' ? payload.showOrnament : true;
|
||||
|
||||
const alignClass = align === 'center' ? 'text-center items-center mx-auto' : align === 'right' ? 'text-right items-end ml-auto' : 'text-left items-start';
|
||||
|
||||
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="text-pop-pink font-medium italic">
|
||||
{accent}
|
||||
</span>
|
||||
{rest}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span key={i}>{part}</span>;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative w-full h-[80vh] min-h-[600px] bg-white flex items-center overflow-hidden">
|
||||
<div className="absolute inset-0 w-full h-full">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={title || 'Hero'}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/90 via-white/50 to-transparent pointer-events-none" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-7xl mx-auto px-6 md:px-12">
|
||||
<div className={`max-w-xl flex flex-col gap-6 ${alignClass}`}>
|
||||
{tagline && (
|
||||
<div className="flex items-center gap-4">
|
||||
{showOrnament && align !== 'right' && <div className="w-8 h-[1px] bg-pop-pink" />}
|
||||
<span className="text-sm font-semibold tracking-[0.2em] uppercase text-pop-pink">
|
||||
{tagline}
|
||||
</span>
|
||||
{showOrnament && align !== 'left' && <div className="w-8 h-[1px] bg-pop-pink" />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-serif text-pop-black leading-tight">
|
||||
{renderTitle(title || '')}
|
||||
</h1>
|
||||
|
||||
{showOrnament && (
|
||||
<div className="flex justify-center items-center text-pop-pink my-2">
|
||||
<Sparkles className="w-5 h-5" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{description && (
|
||||
<p className="text-base md:text-lg text-pop-black/80 leading-relaxed max-w-md">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className={`flex flex-col sm:flex-row items-center gap-4 mt-4 w-full sm:w-auto ${align === 'center' ? 'justify-center' : align === 'right' ? 'justify-end' : 'justify-start'}`}>
|
||||
{primaryBtnText && primaryBtnLink && (
|
||||
<Link
|
||||
to={primaryBtnLink}
|
||||
className="inline-flex items-center justify-center gap-2 px-8 py-3.5 bg-pop-pink text-white font-medium rounded-md hover:bg-pop-pink/90 transition-colors w-full sm:w-auto"
|
||||
>
|
||||
{primaryBtnText}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
)}
|
||||
{secondaryBtnText && secondaryBtnLink && (
|
||||
<Link
|
||||
to={secondaryBtnLink}
|
||||
className="inline-flex items-center justify-center gap-2 px-8 py-3.5 bg-transparent border border-pop-pink text-pop-pink font-medium rounded-md hover:bg-pop-pink/5 transition-colors w-full sm:w-auto"
|
||||
>
|
||||
{secondaryBtnText}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { PageSection, Category, AccordionSectionPayload, ContactInfoSectionPayload, ContactFormSectionPayload } from '@homaroma/types';
|
||||
import DynamicHero from './DynamicHero';
|
||||
import DynamicHeroV2 from './DynamicHeroV2';
|
||||
import DynamicUspBar from './DynamicUspBar';
|
||||
import DynamicCategories from './DynamicCategories';
|
||||
import DynamicProducts from './DynamicProducts';
|
||||
import DynamicBanner from './DynamicBanner';
|
||||
@@ -40,6 +42,10 @@ export default function DynamicPageRenderer({
|
||||
return <DynamicAnnouncementBar key={section.id} payload={section.payload} />;
|
||||
case 'HERO':
|
||||
return <DynamicHero key={section.id} payload={section.payload} />;
|
||||
case 'HERO_V2':
|
||||
return <DynamicHeroV2 key={section.id} payload={section.payload} />;
|
||||
case 'USP_BAR':
|
||||
return <DynamicUspBar key={section.id} payload={section.payload} />;
|
||||
|
||||
// animate เมื่อ scroll เข้า viewport
|
||||
case 'NEWSLETTER':
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getLocalizedField } from '../utils/localization';
|
||||
import { Leaf, Droplet, Sparkles, Gift, CheckCircle, ShieldCheck, Heart } from 'lucide-react';
|
||||
|
||||
const ICON_MAP: Record<string, React.ElementType> = {
|
||||
leaf: Leaf,
|
||||
droplet: Droplet,
|
||||
sparkles: Sparkles,
|
||||
gift: Gift,
|
||||
check: CheckCircle,
|
||||
shield: ShieldCheck,
|
||||
heart: Heart,
|
||||
};
|
||||
|
||||
interface DynamicUspBarProps {
|
||||
payload: Record<string, any>;
|
||||
}
|
||||
|
||||
export default function DynamicUspBar({ payload }: DynamicUspBarProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
const items = Array.isArray(payload.items) ? payload.items : [];
|
||||
const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : 'var(--surface-page)';
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="w-full py-8 border-t border-b border-pop-pink/10" style={{ backgroundColor }}>
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className={`grid grid-cols-1 sm:grid-cols-2 md:grid-cols-${Math.min(items.length, 4)} gap-6 divide-y sm:divide-y-0 sm:divide-x divide-pop-pink/20`}>
|
||||
{items.slice(0, 4).map((item: any, idx: number) => {
|
||||
const Icon = ICON_MAP[item.icon] || CheckCircle;
|
||||
return (
|
||||
<div key={idx} className="flex items-start gap-4 px-4 pt-4 sm:pt-0 first:pt-0">
|
||||
<div className="flex-shrink-0 text-pop-pink">
|
||||
<Icon className="w-8 h-8" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm md:text-base font-semibold text-pop-black">
|
||||
{getLocalizedField(item.title, lang)}
|
||||
</h3>
|
||||
{item.subtitle && (
|
||||
<p className="text-xs md:text-sm text-pop-black/70 mt-1">
|
||||
{getLocalizedField(item.subtitle, lang)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,11 @@ export default function ThemeInjector() {
|
||||
'--color-pop-bg': tokens.surfacePage,
|
||||
'--color-pop-black': tokens.textMain,
|
||||
'--color-night-900': tokens.surfaceCard,
|
||||
'--color-pop-caption': tokens.captionText,
|
||||
'--color-pop-border': tokens.borderColor,
|
||||
'--color-pop-muted-bg': tokens.surfaceMuted,
|
||||
'--radius-base': tokens.radiusBase,
|
||||
'--radius-card': tokens.radiusCard,
|
||||
}
|
||||
for (const [key, value] of Object.entries(mapping)) {
|
||||
if (value && typeof value === 'string' && value.trim() !== '') {
|
||||
|
||||
Reference in new issue
Block a user