import React from 'react'; import { useParams, Link } from '@/spa/router'; import { ArrowLeft, Clock, User, Tag, ArrowRight } from 'lucide-react'; import { motion } from 'framer-motion'; import { getArticleBySlug, articles, type BlogArticle, type BlogSection } from '@/spa/data/blog'; import { useCmsRoute } from '@/spa/cmsRoute'; import { docImageUrl, mediaUrl } from '@/spa/cmsMediaField'; import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { blogDetailContent } from '@/spa/blogDetailContent'; import Image from '@/spa/components/ui/UnoptimizedImage' const NAVY = '#111D55'; const GOLD = '#EFBF04'; const PAGE_BG = '#fff'; const INK = '#3A3A3A'; const FF = '"IBM Plex Sans", sans-serif'; const FB = '"Inter", sans-serif'; type BlogDetailCms = Omit, 'categoryColors'> & { fallbackImage?: unknown categoryColors?: unknown metaCopy?: Partial } const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback; const colorMap = (value: unknown): Record => { if (!value || typeof value !== 'object') return blogDetailContent.categoryColors; return { ...blogDetailContent.categoryColors, ...(value as Record) }; }; const getDetailPage = (value: unknown): BlogDetailCms => { const detail = (value && typeof value === 'object' ? value : {}) as BlogDetailCms; return { ...blogDetailContent, ...detail, notFound: { ...blogDetailContent.notFound, ...detail.notFound }, heroBackLink: { ...blogDetailContent.heroBackLink, ...detail.heroBackLink }, fallbackArticle: { ...blogDetailContent.fallbackArticle, ...detail.fallbackArticle }, categoryColors: colorMap(detail.categoryColors), metaCopy: { ...blogDetailContent.meta, ...(detail.metaCopy || detail.meta) }, sidebarCta: { ...blogDetailContent.sidebarCta, ...detail.sidebarCta }, related: { ...blogDetailContent.related, ...detail.related }, }; }; export default function BlogDetail() { const isMobile = useIsMobile(); const { slug } = useParams<{ slug: string }>(); const route = useCmsRoute(); const cmsArticle = route?.collection === 'posts' ? route.doc : undefined; const cmsDetail = getDetailPage(cmsArticle?.detailPage); const fallbackArticle = cmsDetail.fallbackArticle || blogDetailContent.fallbackArticle; const articleFallbackImage = mediaUrl(cmsDetail.fallbackImage, `/images/${blogDetailContent.fallbackImageFilename}`); const article = (slug ? getArticleBySlug(slug) : undefined) || (cmsArticle ? { slug: String(cmsArticle.slug || slug), title: String(cmsArticle.title || fallbackArticle.title || blogDetailContent.fallbackArticle.title), cat: String(cmsArticle.cat || fallbackArticle.category || blogDetailContent.fallbackArticle.category), date: String(cmsArticle.date || fallbackArticle.date || blogDetailContent.fallbackArticle.date), author: String(cmsArticle.authorName || fallbackArticle.authorName || blogDetailContent.fallbackArticle.authorName), authorRole: String(cmsArticle.authorRole || fallbackArticle.authorRole || blogDetailContent.fallbackArticle.authorRole), readTime: String(cmsArticle.readTime || fallbackArticle.readTime || blogDetailContent.fallbackArticle.readTime), img: docImageUrl(cmsArticle, articleFallbackImage, 'heroImage'), excerpt: String(cmsArticle.excerpt || cmsArticle.meta?.description || fallbackArticle.excerpt || blogDetailContent.fallbackArticle.excerpt), sections: Array.isArray(cmsArticle.sections) && cmsArticle.sections.length > 0 ? cmsArticle.sections as BlogSection[] : [{ body: String(cmsArticle.excerpt || cmsArticle.meta?.description || fallbackArticle.excerpt || blogDetailContent.fallbackArticle.excerpt) }], tags: Array.isArray(cmsArticle.tags) ? cmsArticle.tags .map((item: { tag?: string }) => item.tag) .filter((tag): tag is string => Boolean(tag)) : [String(fallbackArticle.tag || blogDetailContent.fallbackArticle.tag)], relatedSlugs: Array.isArray(cmsArticle.relatedSlugs) ? cmsArticle.relatedSlugs .map((item: { slug?: string }) => item.slug) .filter((slug): slug is string => Boolean(slug)) : [], detailPage: cmsArticle.detailPage, } as BlogArticle : undefined); const detail = getDetailPage(article && 'detailPage' in article ? article.detailPage : cmsDetail); const notFound = detail.notFound || blogDetailContent.notFound; const heroBackLink = detail.heroBackLink || blogDetailContent.heroBackLink; const categoryColors = colorMap(detail.categoryColors); const metaCopy = detail.metaCopy || blogDetailContent.meta; const sidebarCta = detail.sidebarCta || blogDetailContent.sidebarCta; const relatedCopy = detail.related || blogDetailContent.related; const sidebarCtaUrl = fallbackText(sidebarCta.url, blogDetailContent.sidebarCta.url); const sidebarCtaIsExternal = /^(mailto:|tel:|https?:)/.test(sidebarCtaUrl); const sidebarCtaStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, padding: '13px 20px', background: NAVY, color: '#fff', fontFamily: FF, fontSize: 14, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', textDecoration: 'none', transition: 'background 0.15s', }; if (!article) { return (

{fallbackText(notFound.title, blogDetailContent.notFound.title)}

{fallbackText(notFound.backLabel, blogDetailContent.notFound.backLabel)}
); } const related = article.relatedSlugs .map(s => articles.find(a => a.slug === s)) .filter(Boolean) as typeof articles; return (
{/* ── HERO ──────────────────────────────────────────────────────────── */}
{article.title} {/* Gradient overlay */}
{/* Gold bottom line */}
{/* Back link */} (e.currentTarget.style.color = '#fff')} onMouseLeave={e => (e.currentTarget.style.color = 'rgba(255,255,255,0.5)')} > {fallbackText(heroBackLink.label, blogDetailContent.heroBackLink.label)} {/* Category badge */}
{article.cat}
{article.title} {/* Meta row */}
{[ { icon: User, label: `${article.author} · ${article.authorRole}` }, { icon: Clock, label: `${article.date} · ${article.readTime} ${fallbackText(metaCopy.readTimeSuffix, blogDetailContent.meta.readTimeSuffix)}` }, ].map(({ icon: Icon, label }, i) => (
{label}
))}
{/* ── ARTICLE BODY ─────────────────────────────────────────────────── */}
{/* LEFT – article text */}
{/* Lead */}

{article.excerpt}

{/* Sections */} {article.sections.map((section, idx) => ( {section.heading && (

{section.heading}

)}

{section.body}

))} {/* Tags */}
{article.tags.map(tag => ( {tag} ))}
{/* RIGHT – sticky sidebar */}
{/* ── RELATED ARTICLES ─────────────────────────────────────────────── */} {related.length > 0 && (
{fallbackText(relatedCopy.heading, blogDetailContent.related.heading)}
{related.map((rel, idx) => ( 0} isMobile={isMobile} readMoreLabel={fallbackText(relatedCopy.readMoreLabel, blogDetailContent.related.readMoreLabel)} /> ))}
)}
); } function RelatedCard({ article, bordered, isMobile, readMoreLabel }: { article: ReturnType & object; bordered: boolean; isMobile: boolean; readMoreLabel: string }) { const [hovered, setHovered] = React.useState(false); if (!article) return null; return ( setHovered(true)} onMouseLeave={() => setHovered(false)} style={{ display: 'flex', flexDirection: 'column', borderLeft: bordered && !isMobile ? '1px solid rgba(255,255,255,0.07)' : 'none', borderBottom: 'none', textDecoration: 'none', paddingLeft: bordered && !isMobile ? 40 : 0, paddingRight: isMobile ? 0 : 40, paddingBottom: isMobile ? 40 : 0, }} > {/* Thumbnail */}
{article.title}
{article.cat}

{article.title}

{article.excerpt}

{readMoreLabel} ); }