Files
supabase/apps/www/components/Blog/BlogPostRenderer.tsx
Alan Daniel 4eee5c6506 fix(www): prettier formatting and TypeScript errors
- Run prettier on 23 www files flagged by CI format check
- Fix generic type args: Record, ReturnType<typeof setInterval/setTimeout>
- Type optional `badge` field on database HighlightsSection data
- Pass canvas as parameter to async init in PostgresGlow to preserve null narrowing
- Accept readonly arrays in VisualDemo's pick<T>()
- Guard BlogPostRenderer hero image render on imageUrl presence
2026-05-27 12:46:11 -04:00

288 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import dayjs from 'dayjs'
import { ArrowLeft } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import ReactMarkdown from 'react-markdown'
import type { PostReturnType, ProcessedBlogData, StaticAuthor, Tag } from 'types/post'
import { Badge } from 'ui'
import { CTASection } from '@/app/(home)/_components/CTASection'
import DraftModeBanner from '@/components/Blog/DraftModeBanner'
import ShareArticleActions from '@/components/Blog/ShareArticleActions'
import BlogLinks from '@/components/LaunchWeek/7/BlogLinks'
import LW11Summary from '@/components/LaunchWeek/11/LW11Summary'
import LW12Summary from '@/components/LaunchWeek/12/LWSummary'
import LW13Summary from '@/components/LaunchWeek/13/Releases/LWSummary'
import LW14Summary from '@/components/LaunchWeek/14/Releases/LWSummary'
import LW15Summary from '@/components/LaunchWeek/15/LWSummary'
import LWXSummary from '@/components/LaunchWeek/X/LWXSummary'
import { getBlogThumbnailImage } from '@/lib/blog-images'
import { compileBlogMdx } from '@/lib/mdx/compileBlogMdx'
import mdxComponents from '@/lib/mdx/mdxComponents'
type NextCardProps = {
post: { path: string; title: string; formattedDate: string }
label: string
className?: string
}
const NextCard = (props: NextCardProps) => {
const { post, label, className } = props
return (
<Link href={`${post.path}`} as={`${post.path}`}>
<div className={className ?? ''}>
<div className="hover:bg-control cursor-pointer rounded-sm border p-6 transition">
<div className="space-y-4">
<div>
<p className="text-foreground-lighter text-sm">{label}</p>
</div>
<div className="flex flex-col gap-2">
{'title' in post && (
<h4 className="text-foreground text-lg text-balance">
{(post as { title?: string }).title}
</h4>
)}
{'formattedDate' in post && (
<p className="small">{(post as { formattedDate?: string }).formattedDate}</p>
)}
</div>
</div>
</div>
</div>
</Link>
)
}
const BlogPostRenderer = async ({
blog,
blogMetaData,
isDraftMode,
prevPost,
nextPost,
authors,
}: {
blog: ProcessedBlogData
blogMetaData: ProcessedBlogData
isDraftMode: boolean
prevPost?: PostReturnType | null
nextPost?: PostReturnType | null
authors: Array<StaticAuthor>
}) => {
const mdxRendered = await compileBlogMdx(blog.content as string, mdxComponents('blog'))
const isLaunchWeek7 = blogMetaData.launchweek === '7'
const isLaunchWeekX = blogMetaData.launchweek?.toString().toLocaleLowerCase() === 'x'
const isGAWeek = blogMetaData.launchweek?.toString().toLocaleLowerCase() === '11'
const isLaunchWeek12 = blogMetaData.launchweek?.toString().toLocaleLowerCase() === '12'
const isLaunchWeek13 = blogMetaData.launchweek?.toString().toLocaleLowerCase() === '13'
const isLaunchWeek14 = blogMetaData.launchweek?.toString().toLocaleLowerCase() === '14'
const isLaunchWeek15 = blogMetaData.launchweek?.toString().toLocaleLowerCase() === '15'
const toc = blogMetaData.toc && (
<div>
<p className="text-foreground-lighter text-sm font-normal mb-3">On this page</p>
<div className="prose-toc">
{blogMetaData.toc && (
<ReactMarkdown>
{typeof blogMetaData.toc === 'string'
? (blogMetaData.toc as string)
: (blogMetaData.toc as { content: string }).content}
</ReactMarkdown>
)}
</div>
</div>
)
const imageUrl = getBlogThumbnailImage(blogMetaData, {
fallbackToPlaceholder: false,
})
return (
<>
{isDraftMode && <DraftModeBanner />}
<div className="overflow-x-clip">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6">
<div className="grid grid-cols-12 xl:gap-x-8 xl:gap-y-0">
{/* Row 1, Col 1: Back button */}
<div className="col-span-12 xl:row-start-1 xl:col-start-1 xl:col-span-1 pt-16 xl:pt-32 flex items-start justify-start">
<Link
href="/blog"
className="text-foreground-lighter hover:text-foreground inline-flex cursor-pointer items-center text-sm transition"
>
<ArrowLeft strokeWidth={1.5} className="size-4" />
Blog
</Link>
</div>
{/* Row 1, Col 2: Title + meta */}
<div className="col-span-12 xl:row-start-1 xl:col-start-2 xl:col-span-7 pt-4 xl:pt-32 pb-6 space-y-4">
<div className="space-y-4">
{(blogMetaData.tags as Tag[])?.length > 0 && (
<div className="flex flex-wrap gap-2">
{(blogMetaData.tags as Tag[])?.map((tag) => {
const tagName = typeof tag === 'string' ? tag : tag.name
const tagId = typeof tag === 'string' ? tag : tag.id.toString()
return (
<Link
className="flex"
href={`/blog/tags/${tagName}`}
key={`category-badge-${tagId}`}
>
<Badge>{tagName}</Badge>
</Link>
)
})}
</div>
)}
<h1 className="text-2xl sm:text-4xl max-w-3xl">{blogMetaData.title}</h1>
<div className="flex items-center gap-2">
<div className="text-foreground-lighter flex space-x-3 text-sm">
<p>{dayjs(blogMetaData.date).format('DD MMM YYYY')}</p>
<p>·</p>
<p>{(blogMetaData as any).readingTime}</p>
</div>
</div>
{authors.length > 0 && (
<div className="flex items-center gap-4 mt-6">
{authors.map((author, i: number) => {
const authorImageUrl = author.author_image_url
const authorId =
(author as any).author_id ||
(author as any).username ||
author.author.toLowerCase().replace(/\s+/g, '_')
return (
<Link
href={`/blog/authors/${authorId}`}
className="cursor-pointer"
key={`author-${i}-${author.author}`}
>
<div className="flex items-center gap-2">
{authorImageUrl && (
<Image
src={authorImageUrl}
className="border-default rounded-full border aspect-square object-cover"
alt={`${author.author} avatar`}
width={32}
height={32}
/>
)}
<div className="flex flex-col">
<span className="text-foreground text-sm">{author.author}</span>
<span className="text-foreground-lighter text-xs">
{author.position}
</span>
</div>
</div>
</Link>
)
})}
</div>
)}
</div>
</div>
{/* Row 2: Hero image — col-start-1 col-span-9 = equal overhang each side of content (col 2–8) */}
{!blogMetaData.youtubeHero && blogMetaData.imgThumb && imageUrl && (
<div className="col-span-12 xl:row-start-2 xl:col-start-1 xl:col-span-9 mb-6">
<div className="relative w-full aspect-[1.91/1] overflow-hidden rounded-lg border border-foreground/10">
<Image
src={imageUrl}
alt={blogMetaData.title}
fill
quality={100}
sizes="(max-width: 768px) 100vw, 75vw"
className="object-cover"
/>
</div>
</div>
)}
{/* Row 3: Article */}
<div className="col-span-12 xl:row-start-3 xl:col-start-2 xl:col-span-7 pb-16">
<article>
<div className="prose prose-docs">
{blogMetaData.youtubeHero ? (
<iframe
title="YouTube video player"
className="w-full"
width="700"
height="350"
src={blogMetaData.youtubeHero}
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen={true}
/>
) : null}
{mdxRendered}
</div>
</article>
{isLaunchWeek7 && <BlogLinks />}
{isLaunchWeekX && <LWXSummary />}
{isGAWeek && <LW11Summary />}
{isLaunchWeek12 && <LW12Summary />}
{isLaunchWeek13 && <LW13Summary />}
{isLaunchWeek14 && <LW14Summary />}
{isLaunchWeek15 && <LW15Summary />}
<div className="block lg:hidden py-8">
<div className="text-foreground-lighter text-sm">Share this article</div>
<ShareArticleActions title={blogMetaData.title} slug={blogMetaData.slug} />
</div>
<div className="grid gap-8 py-8 lg:grid-cols-1">
<div>
{prevPost && (
<NextCard
post={
prevPost as unknown as {
path: string
title: string
formattedDate: string
}
}
label="Previous post"
/>
)}
</div>
<div>
{nextPost && (
<NextCard
post={
nextPost as unknown as {
path: string
title: string
formattedDate: string
}
}
label="Next post"
className="text-right"
/>
)}
</div>
</div>
</div>
{/* TOC: starts at row 2 alongside image, sticky through article */}
<div className="hidden xl:block xl:row-start-2 xl:row-span-2 xl:col-start-10 xl:col-span-3 xl:pl-6">
<div className="sticky top-24 flex flex-col gap-6 max-h-[calc(100vh-7rem)]">
<div className="overflow-y-auto min-h-0 flex-1">{toc}</div>
<div className="shrink-0">
<div className="text-foreground text-sm mb-2">Share this article</div>
<ShareArticleActions title={blogMetaData.title} slug={blogMetaData.slug} />
</div>
</div>
</div>
</div>
</div>
<div className="border-t">
<CTASection />
</div>
</div>
</>
)
}
export default BlogPostRenderer