diff options
| author | Maze <167211895+mazeincoding@users.noreply.github.com> | 2026-01-31 00:20:04 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-31 00:20:04 +0100 |
| commit | 7bf0984698e43b9aa2dd879f65951318ed410390 (patch) | |
| tree | ccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/app/blog | |
| parent | 0173db99448d1c4c6415dcf15272e17551b48b04 (diff) | |
| download | opencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz | |
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/app/blog')
| -rw-r--r-- | apps/web/src/app/blog/[slug]/page.tsx | 246 | ||||
| -rw-r--r-- | apps/web/src/app/blog/page.tsx | 151 |
2 files changed, 190 insertions, 207 deletions
diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx index d759050b..46c6163d 100644 --- a/apps/web/src/app/blog/[slug]/page.tsx +++ b/apps/web/src/app/blog/[slug]/page.tsx @@ -1,144 +1,152 @@ -import { Header } from "@/components/header"; -import Prose from "@/components/ui/prose"; -import { Separator } from "@/components/ui/separator"; -import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog-query"; -import { Metadata } from "next"; +import type { Metadata } from "next"; import Image from "next/image"; import { notFound } from "next/navigation"; +import { BasePage } from "@/app/base-page"; +import Prose from "@/components/ui/prose"; +import { Separator } from "@/components/ui/separator"; +import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog/query"; +import type { Author, Post } from "@/types/blog"; type PageProps = { - params: Promise<{ slug: string }>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; + params: Promise<{ slug: string }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }; export async function generateMetadata({ - params, + params, }: PageProps): Promise<Metadata> { - const slug = (await params).slug; + const slug = (await params).slug; - const data = await getSinglePost(slug); + const data = await getSinglePost({ slug }); - if (!data || !data.post) return {}; + if (!data || !data.post) return {}; - return { - title: data.post.title, - description: data.post.description, - twitter: { - title: `${data.post.title}`, - description: `${data.post.description}`, - card: "summary_large_image", - images: [ - { - url: data.post.coverImage, - width: "1200", - height: "630", - alt: data.post.title, - }, - ], - }, - openGraph: { - type: "article", - images: [ - { - url: data.post.coverImage, - width: "1200", - height: "630", - alt: data.post.title, - }, - ], - title: data.post.title, - description: data.post.description, - publishedTime: new Date(data.post.publishedAt).toISOString(), - authors: [ - ...data.post.authors.map((author: { name: string }) => author.name), - ], - }, - }; + return { + title: data.post.title, + description: data.post.description, + twitter: { + title: `${data.post.title}`, + description: `${data.post.description}`, + card: "summary_large_image", + images: [ + { + url: data.post.coverImage, + width: "1200", + height: "630", + alt: data.post.title, + }, + ], + }, + openGraph: { + type: "article", + images: [ + { + url: data.post.coverImage, + width: "1200", + height: "630", + alt: data.post.title, + }, + ], + title: data.post.title, + description: data.post.description, + publishedTime: new Date(data.post.publishedAt).toISOString(), + authors: data.post.authors.map((author: Author) => author.name), + }, + }; } export async function generateStaticParams() { - const data = await getPosts(); - if (!data || !data.posts.length) return []; + const data = await getPosts(); + if (!data || !data.posts.length) return []; - return data.posts.map((post) => ({ - slug: post.slug, - })); + return data.posts.map((post) => ({ + slug: post.slug, + })); } -async function Page({ params }: PageProps) { - const slug = (await params).slug; - const data = await getSinglePost(slug); - if (!data || !data.post) return notFound(); +export default async function BlogPostPage({ params }: PageProps) { + const slug = (await params).slug; + const data = await getSinglePost({ slug }); + if (!data || !data.post) return notFound(); - const html = await processHtmlContent(data.post.content); + const html = await processHtmlContent({ html: data.post.content }); - const formattedDate = new Date(data.post.publishedAt).toLocaleDateString( - "en-US", - { - day: "numeric", - month: "long", - year: "numeric", - } - ); + return ( + <BasePage> + <PostHeader post={data.post} /> + <Separator /> + <PostContent html={html} /> + </BasePage> + ); +} - return ( - <div className="min-h-screen bg-background"> - <Header /> +function PostHeader({ post }: { post: Post }) { + const formattedDate = new Date(post.publishedAt).toLocaleDateString("en-US", { + day: "numeric", + month: "long", + year: "numeric", + }); - <main className="relative"> - <div className="absolute inset-0 overflow-hidden pointer-events-none"> - <div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" /> - <div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" /> - </div> + return ( + <> + {post.coverImage && <PostCoverImage post={post} />} + <PostMeta date={formattedDate} publishedAt={post.publishedAt} /> + <PostTitle title={post.title} /> + <PostAuthor author={post.authors[0]} /> + </> + ); +} - <div className="relative container max-w-3xl mx-auto px-4 py-16"> - <div className="text-center mb-6"> - {data.post.coverImage && ( - <div className="relative aspect-video rounded-lg overflow-hidden mb-6"> - <Image - src={data.post.coverImage} - alt={data.post.title} - loading="eager" - fill - className="object-cover rounded-lg" - /> - </div> - )} - <div className="flex items-center justify-center mb-6"> - <time dateTime={data.post.publishedAt.toString()}> - {formattedDate} - </time> - </div> +function PostCoverImage({ post }: { post: Post }) { + return ( + <div className="relative aspect-video overflow-hidden rounded-lg"> + <Image + src={post.coverImage} + alt={post.title} + loading="eager" + fill + className="rounded-lg object-cover" + /> + </div> + ); +} - <h1 className="text-5xl md:text-4xl font-bold tracking-tight mb-6"> - {data.post.title} - </h1> - <div className="flex items-center justify-center gap-2"> - {data.post.authors[0] && ( - <> - <Image - src={data.post.authors[0].image} - alt={data.post.authors[0].name} - width={36} - height={36} - loading="eager" - className="aspect-square shrink-0 size-8 rounded-full" - /> - <p className="text-muted-foreground"> - {data.post.authors[0].name} - </p> - </> - )} - </div> - </div> - <Separator /> - <section className="mt-14"> - <Prose html={html} /> - </section> - </div> - </main> - </div> - ); +function PostMeta({ date, publishedAt }: { date: string; publishedAt: Date }) { + return ( + <div className="flex items-center justify-center"> + <time dateTime={publishedAt.toString()}>{date}</time> + </div> + ); } -export default Page; +function PostTitle({ title }: { title: string }) { + return ( + <h1 className="text-5xl font-bold tracking-tight md:text-4xl">{title}</h1> + ); +} + +function PostAuthor({ author }: { author?: Author }) { + if (!author) return null; + + return ( + <div className="flex items-center justify-center gap-2"> + <Image + src={author.image} + alt={author.name} + width={36} + height={36} + loading="eager" + className="aspect-square size-8 shrink-0 rounded-full" + /> + <p className="text-muted-foreground">{author.name}</p> + </div> + ); +} + +function PostContent({ html }: { html: string }) { + return ( + <section className="pt-8"> + <Prose html={html} /> + </section> + ); +} diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx index 08da9fd7..9e3b6d11 100644 --- a/apps/web/src/app/blog/page.tsx +++ b/apps/web/src/app/blog/page.tsx @@ -1,98 +1,73 @@ -import { Metadata } from "next"; -import { Header } from "@/components/header"; -import { Card, CardContent } from "@/components/ui/card"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import type { Metadata } from "next"; import Link from "next/link"; -import { getPosts } from "@/lib/blog-query"; -import Image from "next/image"; +import { BasePage } from "@/app/base-page"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Separator } from "@/components/ui/separator"; +import { getPosts } from "@/lib/blog/query"; +import type { Author, Post } from "@/types/blog"; export const metadata: Metadata = { - title: "Blog - OpenCut", - description: - "Read the latest news and updates about OpenCut, the free and open-source video editor.", - openGraph: { - title: "Blog - OpenCut", - description: - "Read the latest news and updates about OpenCut, the free and open-source video editor.", - type: "website", - }, + title: "Blog - OpenCut", + description: + "Read the latest news and updates about OpenCut, the free and open-source video editor.", + openGraph: { + title: "Blog - OpenCut", + description: + "Read the latest news and updates about OpenCut, the free and open-source video editor.", + type: "website", + }, }; export default async function BlogPage() { - const data = await getPosts(); - if (!data || !data.posts) return <div>No posts yet</div>; - - return ( - <div className="min-h-screen bg-background"> - <Header /> + const data = await getPosts(); + if (!data || !data.posts) return <div>No posts yet</div>; - <main className="relative"> - <div className="absolute inset-0 overflow-hidden pointer-events-none"> - <div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" /> - <div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" /> - </div> + return ( + <BasePage + title="Blog" + description="Read the latest news and updates about OpenCut, the free and open-source video editor." + > + <div className="flex flex-col"> + {data.posts.map((post) => ( + <div key={post.id} className="flex flex-col"> + <BlogPostItem post={post} /> + <Separator /> + </div> + ))} + </div> + </BasePage> + ); +} - <div className="relative container max-w-3xl mx-auto px-4 py-16"> - <div className="text-center mb-20"> - <h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6"> - Blog - </h1> - <p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed"> - Read the latest news and updates about OpenCut, the free and - open-source video editor. - </p> - </div> - <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> - {data.posts.map((post) => ( - <Link key={post.id} href={`/blog/${post.slug}`}> - <Card className="h-full hover:shadow-lg transition-shadow overflow-hidden"> - {post.coverImage && ( - <div className="relative aspect-video"> - <Image - src={post.coverImage} - alt={post.title} - fill - className="object-cover rounded-xl" - /> - </div> - )} +function BlogPostItem({ post }: { post: Post }) { + return ( + <Link href={`/blog/${post.slug}`}> + <div className="flex h-auto w-full items-center justify-between py-6 opacity-100 hover:opacity-75"> + <div className="flex flex-col gap-2"> + <h2 className="text-xl font-semibold">{post.title}</h2> + <p className="text-muted-foreground">{post.description}</p> + </div> + {post.authors && post.authors.length > 0 && ( + <AuthorList authors={post.authors} /> + )} + </div> + </Link> + ); +} - <CardContent className="p-6"> - {post.authors && post.authors.length > 0 && ( - <div className="flex items-center gap-2 mb-4"> - {post.authors.map((author, index) => ( - <div - key={author.id} - className="flex items-center gap-2" - > - <Avatar className="w-6 h-6"> - <AvatarImage - src={author.image} - alt={author.name} - /> - <AvatarFallback className="text-xs"> - {author.name.charAt(0).toUpperCase()} - </AvatarFallback> - </Avatar> - <span className="text-sm text-muted-foreground"> - {author.name} - </span> - {index < post.authors.length - 1 && ( - <span className="text-muted-foreground">•</span> - )} - </div> - ))} - </div> - )} - <h2 className="text-xl font-semibold mb-2">{post.title}</h2> - <p className="text-muted-foreground">{post.description}</p> - </CardContent> - </Card> - </Link> - ))} - </div> - </div> - </main> - </div> - ); +function AuthorList({ authors }: { authors: Author[] }) { + return ( + <div className="flex items-center gap-2"> + {authors.map((author) => ( + <div key={author.id} className="flex items-center gap-2"> + <Avatar className="size-6 shadow-sm"> + <AvatarImage src={author.image} alt={author.name} /> + <AvatarFallback className="text-xs"> + {author.name.charAt(0).toUpperCase()} + </AvatarFallback> + </Avatar> + </div> + ))} + </div> + ); } |
