feat(content): add announcements and public news flows

This commit is contained in:
2026-02-12 20:08:08 +01:00
parent 994b33e081
commit dbf817c255
20 changed files with 1071 additions and 8 deletions

View File

@@ -3,7 +3,7 @@ import { notFound } from "next/navigation"
import { hasLocale, NextIntlClientProvider } from "next-intl"
import { getTranslations } from "next-intl/server"
import type { ReactNode } from "react"
import { PublicAnnouncements } from "@/components/public-announcements"
import { PublicHeaderBanner } from "@/components/public-header-banner"
import { PublicSiteFooter } from "@/components/public-site-footer"
import { PublicSiteHeader } from "@/components/public-site-header"
@@ -52,6 +52,7 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
<NextIntlClientProvider locale={locale}>
<Providers>
<PublicHeaderBanner banner={banner} />
<PublicAnnouncements placement="global_top" />
<PublicSiteHeader />
<main>{children}</main>
<PublicSiteFooter />

View File

@@ -0,0 +1,30 @@
import { getPostBySlug } from "@cms/db"
import { notFound } from "next/navigation"
export const dynamic = "force-dynamic"
type PageProps = {
params: Promise<{ slug: string }>
}
export default async function PublicNewsDetailPage({ params }: PageProps) {
const { slug } = await params
const post = await getPostBySlug(slug)
if (!post || post.status !== "published") {
notFound()
}
return (
<article className="mx-auto w-full max-w-4xl space-y-4 px-6 py-16">
<header className="space-y-2">
<p className="text-sm uppercase tracking-[0.2em] text-neutral-500">News</p>
<h1 className="text-4xl font-semibold tracking-tight">{post.title}</h1>
{post.excerpt ? <p className="text-neutral-600">{post.excerpt}</p> : null}
</header>
<section className="prose prose-neutral max-w-none whitespace-pre-wrap rounded-xl border border-neutral-200 bg-white p-6 text-neutral-800">
{post.body}
</section>
</article>
)
}

View File

@@ -0,0 +1,33 @@
import { listPosts } from "@cms/db"
import Link from "next/link"
export const dynamic = "force-dynamic"
export default async function PublicNewsIndexPage() {
const posts = await listPosts()
return (
<section className="mx-auto w-full max-w-4xl space-y-4 px-6 py-16">
<header className="space-y-2">
<p className="text-sm uppercase tracking-[0.2em] text-neutral-500">News</p>
<h1 className="text-4xl font-semibold tracking-tight">Latest updates</h1>
</header>
<div className="space-y-3">
{posts.map((post) => (
<article key={post.id} className="rounded-lg border border-neutral-200 p-4">
<p className="text-xs uppercase tracking-wide text-neutral-500">{post.status}</p>
<h2 className="mt-1 text-lg font-medium">{post.title}</h2>
<p className="mt-2 text-sm text-neutral-600">{post.excerpt ?? "No excerpt"}</p>
<Link
href={`/news/${post.slug}`}
className="mt-2 inline-block text-sm underline underline-offset-2"
>
Read post
</Link>
</article>
))}
</div>
</section>
)
}

View File

@@ -1,7 +1,7 @@
import { getPublishedPageBySlug, listPosts } from "@cms/db"
import { Button } from "@cms/ui/button"
import { getTranslations } from "next-intl/server"
import { PublicAnnouncements } from "@/components/public-announcements"
import { PublicPageView } from "@/components/public-page-view"
export const dynamic = "force-dynamic"
@@ -16,6 +16,7 @@ export default async function HomePage() {
return (
<section>
{homePage ? <PublicPageView page={homePage} /> : null}
<PublicAnnouncements placement="homepage" />
<section className="mx-auto flex w-full max-w-6xl flex-col gap-6 px-6 py-6 pb-16">
<header className="space-y-3">

View File

@@ -0,0 +1,39 @@
import { listActiveAnnouncements, type PublicAnnouncement } from "@cms/db"
import Link from "next/link"
type PublicAnnouncementsProps = {
placement: "global_top" | "homepage"
}
function AnnouncementCard({ announcement }: { announcement: PublicAnnouncement }) {
return (
<article className="rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-900">
<p className="text-xs uppercase tracking-wide text-blue-700">{announcement.title}</p>
<p className="mt-1">{announcement.message}</p>
{announcement.ctaLabel && announcement.ctaHref ? (
<Link
href={announcement.ctaHref}
className="mt-2 inline-block font-medium underline underline-offset-2"
>
{announcement.ctaLabel}
</Link>
) : null}
</article>
)
}
export async function PublicAnnouncements({ placement }: PublicAnnouncementsProps) {
const announcements = await listActiveAnnouncements(placement)
if (announcements.length === 0) {
return null
}
return (
<section className="mx-auto w-full max-w-6xl space-y-2 px-6 py-3">
{announcements.map((announcement) => (
<AnnouncementCard key={announcement.id} announcement={announcement} />
))}
</section>
)
}