This commit is contained in:
Your Name committed 2026-07-24 19:39:38 -04:00
1 parent e509e3f0a2
commit 0d4df55d93
7 files changed
+526 -3

No files matched your search

@@ -469,6 +469,42 @@ function TextSection({ payload, lang }: { payload: Record<string, unknown>, lang
)
}
function ArticleHeaderSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
const title = localizedFromRecord(payload, 'title', resolvedLang)
const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang)
const category = localizedFromRecord(payload, 'category', resolvedLang)
const author = localizedFromRecord(payload, 'author', resolvedLang)
const imageAlt = localizedFromRecord(payload, 'imageAlt', resolvedLang)
const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : ''
const publishDate = typeof payload.publishDate === 'string' ? payload.publishDate : ''
if (!title) return null
return (
<header className="bg-surface-page ui-section-band">
<div className="mx-auto max-w-6xl">
<div className="mx-auto max-w-4xl text-center">
{category && <p className="mb-4 text-sm font-semibold uppercase text-brand-primary">{category}</p>}
<h1 className="text-4xl font-semibold leading-tight text-text-main md:text-6xl">{title}</h1>
{subtitle && <p className="mx-auto mt-6 max-w-3xl text-lg leading-8 text-muted-text md:text-xl">{subtitle}</p>}
{(author || publishDate) && (
<p className="mt-6 text-sm text-muted-text">
{[author, publishDate].filter(Boolean).join(' · ')}
</p>
)}
</div>
{imageUrl && (
<div className="relative mt-10 aspect-[16/9] w-full overflow-hidden rounded-lg bg-surface-card">
<Image src={imageUrl} alt={imageAlt || title} fill unoptimized priority sizes="100vw" className="object-cover" />
</div>
)}
</div>
</header>
)
}
function ArticleMediaSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
@@ -547,6 +583,159 @@ function ArticleMediaSection({ payload, lang }: { payload: Record<string, unknow
)
}
function ArticleGallerySection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
const title = localizedFromRecord(payload, 'title', resolvedLang)
const images = Array.isArray(payload.images) ? payload.images : []
const featured = payload.layout === 'featured'
if (images.length < 2) return null
return (
<section className="bg-surface-page ui-section-band">
<div className="mx-auto max-w-7xl">
{title && <h2 className="mb-8 text-3xl font-semibold text-text-main md:text-4xl">{title}</h2>}
<div className={featured ? 'grid gap-4 md:grid-cols-2' : 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'}>
{images.map((image, index) => {
const item = toRecord(image)
const imageUrl = typeof item.imageUrl === 'string' ? item.imageUrl : ''
const alt = localizedFromRecord(item, 'alt', resolvedLang)
const caption = localizedFromRecord(item, 'caption', resolvedLang)
if (!imageUrl) return null
return (
<figure key={`${imageUrl}-${index}`} className={featured && index === 0 ? 'md:row-span-2' : undefined}>
<div className={`relative w-full overflow-hidden rounded-lg bg-surface-card ${featured && index === 0 ? 'aspect-[4/5] md:h-full md:min-h-[32rem]' : 'aspect-[4/3]'}`}>
<Image src={imageUrl} alt={alt} fill unoptimized sizes="(max-width: 768px) 100vw, 50vw" className="object-cover" />
</div>
{caption && <figcaption className="mt-3 text-sm leading-6 text-muted-text">{caption}</figcaption>}
</figure>
)
})}
</div>
</div>
</section>
)
}
function ArticleFactsSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
const title = localizedFromRecord(payload, 'title', resolvedLang)
const items = Array.isArray(payload.items) ? payload.items : []
if (items.length === 0) return null
return (
<section className="bg-surface-card ui-section-band">
<div className="mx-auto max-w-6xl">
{title && <h2 className="mb-8 text-3xl font-semibold text-text-main md:text-4xl">{title}</h2>}
<dl className="grid gap-x-10 gap-y-8 md:grid-cols-2">
{items.map((item, index) => {
const record = toRecord(item)
const itemTitle = localizedFromRecord(record, 'title', resolvedLang)
const description = localizedFromRecord(record, 'description', resolvedLang)
return (
<div key={index} className="border-t border-line-primary/20 pt-5">
<dt className="text-lg font-semibold text-text-main">{itemTitle}</dt>
{description && <dd className="mt-2 leading-7 text-muted-text">{description}</dd>}
</div>
)
})}
</dl>
</div>
</section>
)
}
function getVideoEmbedUrl(videoUrl: string) {
try {
const url = new URL(videoUrl)
if (url.hostname === 'youtu.be') return `https://www.youtube.com/embed/${url.pathname.slice(1)}`
if (url.hostname === 'youtube.com' || url.hostname.endsWith('.youtube.com')) {
const id = url.searchParams.get('v')
return id ? `https://www.youtube.com/embed/${id}` : ''
}
if (url.hostname === 'vimeo.com' || url.hostname.endsWith('.vimeo.com')) {
const id = url.pathname.split('/').filter(Boolean).pop()
return id ? `https://player.vimeo.com/video/${id}` : ''
}
} catch {
return ''
}
return ''
}
function ArticleVideoSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
const title = localizedFromRecord(payload, 'title', resolvedLang)
const caption = localizedFromRecord(payload, 'caption', resolvedLang)
const videoUrl = typeof payload.videoUrl === 'string' ? payload.videoUrl : ''
const posterUrl = typeof payload.posterUrl === 'string' ? payload.posterUrl : undefined
const embedUrl = getVideoEmbedUrl(videoUrl)
const aspectClass = payload.aspectRatio === '4:3' ? 'aspect-[4/3]' : payload.aspectRatio === '1:1' ? 'aspect-square' : 'aspect-video'
if (!videoUrl) return null
return (
<section className="bg-surface-page ui-section-band">
<div className="mx-auto max-w-6xl">
{title && <h2 className="mb-8 text-3xl font-semibold text-text-main md:text-4xl">{title}</h2>}
<div className={`${aspectClass} overflow-hidden rounded-lg bg-text-main`}>
{embedUrl ? (
<iframe
src={embedUrl}
title={title || caption || 'Article video'}
className="h-full w-full"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
) : (
<video src={videoUrl} poster={posterUrl} controls preload="metadata" className="h-full w-full object-contain" />
)}
</div>
{caption && <p className="mt-3 text-sm leading-6 text-muted-text">{caption}</p>}
</div>
</section>
)
}
function ArticleReferencesSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
const title = localizedFromRecord(payload, 'title', resolvedLang)
const items = Array.isArray(payload.items) ? payload.items : []
if (items.length === 0) return null
return (
<section className="bg-surface-page ui-section-band">
<div className="mx-auto max-w-4xl">
<h2 className="mb-6 text-2xl font-semibold text-text-main">{title || 'References'}</h2>
<ol className="list-decimal space-y-4 pl-5 text-muted-text">
{items.map((item, index) => {
const record = toRecord(item)
const itemTitle = localizedFromRecord(record, 'title', resolvedLang)
const note = localizedFromRecord(record, 'note', resolvedLang)
const url = typeof record.url === 'string' ? record.url : ''
return (
<li key={index} className="pl-2">
{url ? (
<a href={url} target="_blank" rel="noreferrer" className="font-medium text-text-main underline decoration-line-primary underline-offset-4">
{itemTitle}
</a>
) : <span className="font-medium text-text-main">{itemTitle}</span>}
{note && <p className="mt-1 text-sm leading-6">{note}</p>}
</li>
)
})}
</ol>
</div>
</section>
)
}
function NewsletterSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
@@ -1184,12 +1373,22 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
<TextSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'ARTICLE_HEADER':
return <ArticleHeaderSection key={section.id} payload={payload} lang={lang} />
case 'ARTICLE_MEDIA':
return (
<ScrollReveal key={section.id}>
<ArticleMediaSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'ARTICLE_GALLERY':
return <ArticleGallerySection key={section.id} payload={payload} lang={lang} />
case 'ARTICLE_FACTS':
return <ArticleFactsSection key={section.id} payload={payload} lang={lang} />
case 'ARTICLE_VIDEO':
return <ArticleVideoSection key={section.id} payload={payload} lang={lang} />
case 'ARTICLE_REFERENCES':
return <ArticleReferencesSection key={section.id} payload={payload} lang={lang} />
case 'SCENT_NOTES_BREAKDOWN':
return (
<ScrollReveal key={section.id}>