From c449de248294d64ee58d17c2968991ad4742e9d5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 21 Jun 2026 01:03:29 -0400 Subject: [PATCH] x --- apps/spa/src/App.tsx | 9 - .../src/components/DynamicAnnouncementBar.tsx | 6 +- apps/spa/src/components/DynamicBanner.tsx | 100 ++++--- apps/spa/src/components/DynamicCategories.tsx | 26 +- .../components/DynamicEditorialCarousel.tsx | 24 +- .../src/components/DynamicEditorialTile.tsx | 23 +- apps/spa/src/components/DynamicHero.tsx | 17 +- .../spa/src/components/DynamicHeroArticle.tsx | 21 +- apps/spa/src/components/DynamicNewsletter.tsx | 15 +- .../src/components/DynamicPageRenderer.tsx | 8 +- apps/spa/src/components/DynamicProducts.tsx | 10 +- .../src/components/DynamicQuoteCallout.tsx | 16 +- .../components/DynamicScentNotesBreakdown.tsx | 38 +-- apps/spa/src/components/DynamicText.tsx | 8 +- apps/spa/src/components/Footer.tsx | 40 +-- apps/spa/src/components/Navbar.tsx | 13 +- apps/spa/src/components/ProductCard.tsx | 36 ++- .../components/sections/AccordionSection.tsx | 20 +- .../sections/ContactInfoSection.tsx | 29 +- apps/spa/src/index.css | 152 ++++++++++- apps/website/src/app/[lang]/coupons/page.tsx | 5 +- .../src/app/[lang]/order/[id]/page.tsx | 15 +- apps/website/src/app/globals.css | 255 +++++++++++++++++- .../website/src/components/CheckoutClient.tsx | 21 +- apps/website/src/components/ProfileClient.tsx | 23 +- .../src/components/TrackOrderClient.tsx | 3 +- .../dynamic/DynamicPageRenderer.tsx | 48 ++-- apps/website/src/lib/theme.ts | 13 + 28 files changed, 694 insertions(+), 300 deletions(-) create mode 100644 apps/website/src/lib/theme.ts diff --git a/apps/spa/src/App.tsx b/apps/spa/src/App.tsx index 711eb82..1272142 100644 --- a/apps/spa/src/App.tsx +++ b/apps/spa/src/App.tsx @@ -35,15 +35,6 @@ function App() { useEffect(() => { const lang = i18n.language; document.documentElement.lang = lang; - document.body.classList.remove('font-sans-ja', 'font-sans-zh', 'font-sans-ko'); - - if (lang === 'ja') { - document.body.classList.add('font-sans-ja'); - } else if (lang === 'zh') { - document.body.classList.add('font-sans-zh'); - } else if (lang === 'ko') { - document.body.classList.add('font-sans-ko'); - } }, [i18n.language]); return ( diff --git a/apps/spa/src/components/DynamicAnnouncementBar.tsx b/apps/spa/src/components/DynamicAnnouncementBar.tsx index c8cfbf5..6c510d8 100644 --- a/apps/spa/src/components/DynamicAnnouncementBar.tsx +++ b/apps/spa/src/components/DynamicAnnouncementBar.tsx @@ -15,13 +15,13 @@ export default function DynamicAnnouncementBar({ payload }: DynamicAnnouncementB const { i18n } = useTranslation(); const lang = i18n.language; const { backgroundColor = '#1F3D2D', textColor = '#FDFDF9', speed = 3000 } = payload; - const slides = (payload.slides || []).map((slide: any) => getLocalizedField(slide, lang)); + const slides = (payload.slides || []).map((slide) => getLocalizedField(slide, lang)); if (slides.length === 0) return null; return ( -
- - {/* Background Image */} -
- {title - {/* Overlay gradient */} -
-
+
+ {linkUrl ? ( + +
+ {title +
+
- {/* Content */} -
- {title && ( -

- {title} -

- )} - {description && ( -

- {description} -

- )} - {buttonText && linkUrl && ( -
- - {buttonText} - -
- )} +
+ {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} + {buttonText && ( +
+ + {buttonText} + +
+ )} +
+ + ) : ( +
+
+ {title +
+
+ +
+ {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} + {buttonText && ( +
+ + {buttonText} + +
+ )} +
- + )}
); } diff --git a/apps/spa/src/components/DynamicCategories.tsx b/apps/spa/src/components/DynamicCategories.tsx index f1971d9..91b4505 100644 --- a/apps/spa/src/components/DynamicCategories.tsx +++ b/apps/spa/src/components/DynamicCategories.tsx @@ -26,41 +26,41 @@ export default function DynamicCategories({ payload, categories, loading }: Dyna }, [categories, categoryIds]); return ( -
-
-
-

+
+
+
+

{title || "Our Collections"}

{subtitle && ( -

{subtitle}

+

{subtitle}

)}
{loading ? ( -
+
{Array.from({ length: 5 }).map((_, i) => ( -
+
))}
) : ( -
+
{displayedCategories.map((cat) => (
{typeof { (e.target as HTMLImageElement).style.display = 'none' }} />
- - {typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[localStorage.getItem('homaroma_lang') || 'en'] || (cat.name as any)?.en || ''} + + {getLocalizedField(cat.name, localStorage.getItem('homaroma_lang') || 'en')} {cat._count && ( diff --git a/apps/spa/src/components/DynamicEditorialCarousel.tsx b/apps/spa/src/components/DynamicEditorialCarousel.tsx index 0ec036e..032e606 100644 --- a/apps/spa/src/components/DynamicEditorialCarousel.tsx +++ b/apps/spa/src/components/DynamicEditorialCarousel.tsx @@ -17,7 +17,7 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa const { i18n } = useTranslation(); const lang = i18n.language; const title = getLocalizedField(payload.title, lang); - const slides = (payload.slides || []).map((slide: any) => ({ + const slides = (payload.slides || []).map((slide) => ({ ...slide, title: getLocalizedField(slide.title, lang), description: getLocalizedField(slide.description, lang), @@ -46,17 +46,15 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa }; return ( -
-
- {/* Section Header */} -
-

+
+
+
+

{renderTitle(title) || title}

- {/* Swiper Slider */}
{slides.map((slide, idx) => ( -
- {/* Image wrapper */} +
-
+
- {/* Content details */}
- + {slide.category} -

+

{renderTitle(slide.title) || slide.title}

-

+

{slide.description}

diff --git a/apps/spa/src/components/DynamicEditorialTile.tsx b/apps/spa/src/components/DynamicEditorialTile.tsx index 63b816f..4eb5514 100644 --- a/apps/spa/src/components/DynamicEditorialTile.tsx +++ b/apps/spa/src/components/DynamicEditorialTile.tsx @@ -14,8 +14,8 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr imageUrl, imagePosition = 'left', buttonLink, - backgroundColor = '#FDFDF9', // Default to spa-light - textColor = '#1F3D2D' // Default to spa-forest + backgroundColor = '#FDFDF9', + textColor = '#1F3D2D' } = payload; const title = getLocalizedField(payload.title, lang); const description = getLocalizedField(payload.description, lang); @@ -40,13 +40,12 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr }; return ( -
-
- {/* Image Column */} -
+
- {/* Subtle overlay */}
- {/* Content Column */} -
-

+

{renderTitle(title) || title}

-

+

{description}

{buttonText && buttonLink && (
{buttonText} diff --git a/apps/spa/src/components/DynamicHero.tsx b/apps/spa/src/components/DynamicHero.tsx index de9cda6..bd20188 100644 --- a/apps/spa/src/components/DynamicHero.tsx +++ b/apps/spa/src/components/DynamicHero.tsx @@ -35,7 +35,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) { }; return ( -
+
- {/* Dark gradient overlay for premium vibe */} -
-
+
+
-
+

{tagline || t('hero.defaultTagline')}

-

+

{renderTitle(title) || renderTitle(t('hero.defaultTitle'))}

-

+

{description}

{buttonText || t('hero.exploreCollection')} {t('hero.learnMore')} → diff --git a/apps/spa/src/components/DynamicHeroArticle.tsx b/apps/spa/src/components/DynamicHeroArticle.tsx index dd72dc3..69ae58d 100644 --- a/apps/spa/src/components/DynamicHeroArticle.tsx +++ b/apps/spa/src/components/DynamicHeroArticle.tsx @@ -1,8 +1,13 @@ +import type { HeroArticlePayload } from '@homaroma/types'; import { useTranslation } from 'react-i18next'; import { getLocalizedField } from '../utils/localization'; interface DynamicHeroArticleProps { - payload: any; + payload: HeroArticlePayload & { + imageUrl?: string; + date?: string; + category?: string | { [key: string]: string }; + }; } export default function DynamicHeroArticle({ payload }: DynamicHeroArticleProps) { @@ -16,29 +21,29 @@ export default function DynamicHeroArticle({ payload }: DynamicHeroArticleProps) const category = getLocalizedField(p.category, lang); return ( -
+
{imageUrl && (
{title -
+
)} -
+
{category && ( - + {category} )} -

+

{title || t('newsletter.articleDefaultTitle', 'Scent Guide Article')}

{subtitle && ( -

+

{subtitle}

)} {(author || date) && ( -
+
{author && {t('newsletter.byAuthor', { author })}} {author && date && •} {date && {date}} diff --git a/apps/spa/src/components/DynamicNewsletter.tsx b/apps/spa/src/components/DynamicNewsletter.tsx index d9e41d6..9a6b047 100644 --- a/apps/spa/src/components/DynamicNewsletter.tsx +++ b/apps/spa/src/components/DynamicNewsletter.tsx @@ -53,19 +53,18 @@ export default function DynamicNewsletter({ payload }: DynamicNewsletterProps) { }; return ( -
- {/* Background decorations for luxury aesthetic */} +
-
+
-

+

{title || t('newsletter.defaultTitle')}

-

+

{description}

@@ -76,7 +75,7 @@ export default function DynamicNewsletter({ payload }: DynamicNewsletterProps) {

{t('newsletter.successMessage')}

) : ( -
+
setEmail(e.target.value)} - className="flex-1 px-6 py-4 bg-white/5 border border-white/10 rounded-full text-white placeholder-white/40 focus:outline-none focus:border-spa-gold text-sm transition-all" + className="ui-input-shell flex-1" /> diff --git a/apps/spa/src/components/DynamicPageRenderer.tsx b/apps/spa/src/components/DynamicPageRenderer.tsx index 9bf8ea5..eb9fdd4 100644 --- a/apps/spa/src/components/DynamicPageRenderer.tsx +++ b/apps/spa/src/components/DynamicPageRenderer.tsx @@ -1,4 +1,4 @@ -import type { PageSection, Category } from '@homaroma/types'; +import type { PageSection, Category, AccordionSectionPayload, ContactInfoSectionPayload, ContactFormSectionPayload } from '@homaroma/types'; import DynamicHero from './DynamicHero'; import DynamicCategories from './DynamicCategories'; import DynamicProducts from './DynamicProducts'; @@ -73,11 +73,11 @@ export default function DynamicPageRenderer({ case 'PRODUCT_CAROUSEL': return ; case 'ACCORDION': - return ; + return ; case 'CONTACT_INFO': - return ; + return ; case 'CONTACT_FORM': - return ; + return ; default: console.warn(`[DynamicPageRenderer] Unknown block type: ${section.type}`); return null; diff --git a/apps/spa/src/components/DynamicProducts.tsx b/apps/spa/src/components/DynamicProducts.tsx index e4369c5..405f259 100644 --- a/apps/spa/src/components/DynamicProducts.tsx +++ b/apps/spa/src/components/DynamicProducts.tsx @@ -24,11 +24,11 @@ export default function DynamicProducts({ payload, categories = [] }: DynamicPro const categoryLookup = new Map(categories.map((category) => [category.id, category])); return ( -
-
-
+
+
+
-

+

{title || "Featured Picks"}

{subtitle && ( @@ -45,7 +45,7 @@ export default function DynamicProducts({ payload, categories = [] }: DynamicPro
{products.length === 0 ? ( -
+
No products found in this section
) : layout === 'carousel' ? ( diff --git a/apps/spa/src/components/DynamicQuoteCallout.tsx b/apps/spa/src/components/DynamicQuoteCallout.tsx index f5d7016..6580caf 100644 --- a/apps/spa/src/components/DynamicQuoteCallout.tsx +++ b/apps/spa/src/components/DynamicQuoteCallout.tsx @@ -1,25 +1,25 @@ +import type { QuoteCalloutPayload } from '@homaroma/types'; import { useTranslation } from 'react-i18next'; import { getLocalizedField } from '../utils/localization'; interface DynamicQuoteCalloutProps { - payload: any; + payload: QuoteCalloutPayload & { role?: string }; } export default function DynamicQuoteCallout({ payload }: DynamicQuoteCalloutProps) { const { i18n } = useTranslation(); const lang = i18n.language; - const p = payload || {}; - const quote = getLocalizedField(p.quote, lang); - const author = getLocalizedField(p.author, lang); - const role = getLocalizedField(p.role, lang); + const quote = getLocalizedField(payload.quote, lang); + const author = getLocalizedField(payload.author, lang); + const role = getLocalizedField(payload.role, lang); return ( -
-
+
+
-
+
"{quote || 'Fragrance is the invisible, unforgettable, ultimate accessory of fashion.'}"
{(author || role) && ( diff --git a/apps/spa/src/components/DynamicScentNotesBreakdown.tsx b/apps/spa/src/components/DynamicScentNotesBreakdown.tsx index f57196f..f085f2d 100644 --- a/apps/spa/src/components/DynamicScentNotesBreakdown.tsx +++ b/apps/spa/src/components/DynamicScentNotesBreakdown.tsx @@ -1,17 +1,17 @@ +import type { ScentNotesBreakdownPayload } from '@homaroma/types'; import { useTranslation } from 'react-i18next'; import { getLocalizedField } from '../utils/localization'; interface DynamicScentNotesBreakdownProps { - payload: any; + payload: ScentNotesBreakdownPayload; } export default function DynamicScentNotesBreakdown({ payload }: DynamicScentNotesBreakdownProps) { const { i18n } = useTranslation(); const lang = i18n.language; - const p = payload || {}; - const title = getLocalizedField(p.title, lang); - const description = getLocalizedField(p.description, lang); - const notes = (value: any) => Array.isArray(value) + const title = getLocalizedField(payload.title, lang); + const description = getLocalizedField(payload.description, lang); + const notes = (value: ScentNotesBreakdownPayload['topNotes'] | ScentNotesBreakdownPayload['middleNotes'] | ScentNotesBreakdownPayload['baseNotes']) => Array.isArray(value) ? value.map((item) => getLocalizedField(item, lang)).filter(Boolean).join(', ') : getLocalizedField(value, lang); const labels = { @@ -23,39 +23,39 @@ export default function DynamicScentNotesBreakdown({ payload }: DynamicScentNote ko: { top: '탑 노트', heart: '미들 노트', base: '베이스 노트' } } as const; const labelSet = labels[lang as keyof typeof labels] ?? labels.en; - const topNotes = notes(p.topNotes); - const heartNotes = notes(p.heartNotes ?? p.middleNotes); - const baseNotes = notes(p.baseNotes); + const topNotes = notes(payload.topNotes); + const heartNotes = notes(payload.middleNotes); + const baseNotes = notes(payload.baseNotes); return ( -
-
-

+
+
+

{title || 'Scent Notes'}

{description && ( -

+

{description}

)} -
-
-

{labelSet.top}

+
+
+

{labelSet.top}

{topNotes || 'The initial, lighter smells of the fragrance.'}

-
-

{labelSet.heart}

+
+

{labelSet.heart}

{heartNotes || 'The core of the fragrance, emerging just after the top notes.'}

-
-

{labelSet.base}

+
+

{labelSet.base}

{baseNotes || 'The deepest, richest scents that linger the longest.'}

diff --git a/apps/spa/src/components/DynamicText.tsx b/apps/spa/src/components/DynamicText.tsx index 5a75ed2..df071d0 100644 --- a/apps/spa/src/components/DynamicText.tsx +++ b/apps/spa/src/components/DynamicText.tsx @@ -20,15 +20,15 @@ export default function DynamicText({ payload }: DynamicTextProps) { }[textAlign] || 'text-left'; return ( -
-
+
+
{title && ( -

+

{title}

)}
diff --git a/apps/spa/src/components/Footer.tsx b/apps/spa/src/components/Footer.tsx index 9c4bc8b..776dc09 100644 --- a/apps/spa/src/components/Footer.tsx +++ b/apps/spa/src/components/Footer.tsx @@ -2,10 +2,20 @@ import { Link } from 'react-router-dom' import { useState, useEffect } from 'react' import { API_BASE, type Category, type StoreSettings, type MenuItem } from '../types' import { useTranslation } from 'react-i18next' +import { getLocalizedField } from '../utils/localization' interface FooterProps { categories?: Category[] } +type MenuTranslation = { + langCode?: string + label?: string +} + +type MenuItemWithTranslations = MenuItem & { + translations?: MenuTranslation[] +} + export default function Footer({ categories = [] }: FooterProps) { const { t } = useTranslation() const [exploreMenu, setExploreMenu] = useState([]) @@ -13,10 +23,10 @@ export default function Footer({ categories = [] }: FooterProps) { const [settings, setSettings] = useState(null) const brandName = settings?.brandName || '' - const translateMenuLabel = (item: MenuItem) => { + const translateMenuLabel = (item: MenuItemWithTranslations) => { const lang = localStorage.getItem('homaroma_lang') || 'en' if (lang !== 'en' && item.translations && item.translations.length > 0) { - const trans = item.translations.find((t: any) => t.langCode === lang) + const trans = item.translations.find((translation) => translation.langCode === lang) if (trans && trans.label) { return trans.label } @@ -41,14 +51,14 @@ export default function Footer({ categories = [] }: FooterProps) { fetch(`${API_BASE}/menus/footer-explore`) .then(r => r.ok ? r.json() : null) .then(data => { - if (data && data.items) setExploreMenu(data.items.sort((a: any, b: any) => a.order - b.order)) + if (data && data.items) setExploreMenu(data.items.sort((a: MenuItem, b: MenuItem) => a.order - b.order)) }) .catch(console.error) fetch(`${API_BASE}/menus/footer-support`) .then(r => r.ok ? r.json() : null) .then(data => { - if (data && data.items) setSupportMenu(data.items.sort((a: any, b: any) => a.order - b.order)) + if (data && data.items) setSupportMenu(data.items.sort((a: MenuItem, b: MenuItem) => a.order - b.order)) }) .catch(console.error) @@ -61,31 +71,31 @@ export default function Footer({ categories = [] }: FooterProps) { }, []) return ( -