feat(announcements): add locale audience targeting and published-home news

This commit is contained in:
2026-02-12 23:05:39 +01:00
parent 741883465c
commit 60c9035743
10 changed files with 93 additions and 12 deletions

View File

@@ -14,6 +14,7 @@ import { requirePermissionForRoute } from "@/lib/route-guards"
export const dynamic = "force-dynamic"
type SearchParamsInput = Record<string, string | string[] | undefined>
const SUPPORTED_LOCALES = ["de", "en", "es", "fr"] as const
function readFirstValue(value: string | string[] | undefined): string | null {
if (Array.isArray(value)) {
@@ -49,6 +50,22 @@ function readNullableDate(formData: FormData, field: string): Date | null {
return parsed
}
function readLocaleSelections(formData: FormData, field: string): string[] {
const values = formData.getAll(field)
const locales = new Set<string>()
for (const value of values) {
if (
typeof value === "string" &&
SUPPORTED_LOCALES.includes(value as (typeof SUPPORTED_LOCALES)[number])
) {
locales.add(value)
}
}
return Array.from(locales)
}
function readInt(formData: FormData, field: string, fallback = 100): number {
const value = readInputString(formData, field)
@@ -94,6 +111,7 @@ async function createAnnouncementAction(formData: FormData) {
title: readInputString(formData, "title"),
message: readInputString(formData, "message"),
placement: readInputString(formData, "placement"),
targetLocales: readLocaleSelections(formData, "targetLocales"),
priority: readInt(formData, "priority", 100),
ctaLabel: readNullableString(formData, "ctaLabel"),
ctaHref: readNullableString(formData, "ctaHref"),
@@ -125,6 +143,7 @@ async function updateAnnouncementAction(formData: FormData) {
title: readInputString(formData, "title"),
message: readInputString(formData, "message"),
placement: readInputString(formData, "placement"),
targetLocales: readLocaleSelections(formData, "targetLocales"),
priority: readInt(formData, "priority", 100),
ctaLabel: readNullableString(formData, "ctaLabel"),
ctaHref: readNullableString(formData, "ctaHref"),
@@ -277,6 +296,20 @@ export default async function AnnouncementsPage({
/>
</label>
</div>
<div className="space-y-1">
<p className="text-xs text-neutral-600">Target locales (empty = all locales)</p>
<div className="flex flex-wrap gap-3">
{SUPPORTED_LOCALES.map((locale) => (
<label
key={`create-locale-${locale}`}
className="inline-flex items-center gap-2 text-sm"
>
<input name="targetLocales" type="checkbox" value={locale} className="size-4" />
{locale.toUpperCase()}
</label>
))}
</div>
</div>
<div className="grid gap-3 md:grid-cols-2">
<label className="space-y-1">
<span className="text-xs text-neutral-600">Starts at</span>
@@ -390,6 +423,26 @@ export default async function AnnouncementsPage({
/>
</label>
</div>
<div className="mt-3 space-y-1">
<p className="text-xs text-neutral-600">Target locales (empty = all locales)</p>
<div className="flex flex-wrap gap-3">
{SUPPORTED_LOCALES.map((locale) => (
<label
key={`${announcement.id}-locale-${locale}`}
className="inline-flex items-center gap-2 text-sm"
>
<input
name="targetLocales"
type="checkbox"
value={locale}
defaultChecked={announcement.targetLocales.includes(locale)}
className="size-4"
/>
{locale.toUpperCase()}
</label>
))}
</div>
</div>
<div className="mt-3 flex flex-wrap items-center justify-between gap-3">
<label className="inline-flex items-center gap-2 text-sm text-neutral-700">
<input

View File

@@ -52,7 +52,7 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
<NextIntlClientProvider locale={locale}>
<Providers>
<PublicHeaderBanner banner={banner} />
<PublicAnnouncements placement="global_top" />
<PublicAnnouncements placement="global_top" locale={locale} />
<PublicSiteHeader />
<main>{children}</main>
<PublicSiteFooter />

View File

@@ -1,4 +1,4 @@
import { getPublishedPageBySlugForLocale, listPosts } from "@cms/db"
import { getPublishedPageBySlugForLocale, listPostsForLocale } from "@cms/db"
import { getTranslations } from "next-intl/server"
import { PublicAnnouncements } from "@/components/public-announcements"
import { PublicPageView } from "@/components/public-page-view"
@@ -15,14 +15,14 @@ export default async function HomePage({ params }: HomePageProps) {
const [homePage, posts, t] = await Promise.all([
getPublishedPageBySlugForLocale("home", locale),
listPosts(),
listPostsForLocale(locale),
getTranslations("Home"),
])
return (
<section>
{homePage ? <PublicPageView page={homePage} /> : null}
<PublicAnnouncements placement="homepage" />
<PublicAnnouncements placement="homepage" locale={locale} />
<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

@@ -3,6 +3,7 @@ import Link from "next/link"
type PublicAnnouncementsProps = {
placement: "global_top" | "homepage"
locale?: string
}
function AnnouncementCard({ announcement }: { announcement: PublicAnnouncement }) {
@@ -22,8 +23,8 @@ function AnnouncementCard({ announcement }: { announcement: PublicAnnouncement }
)
}
export async function PublicAnnouncements({ placement }: PublicAnnouncementsProps) {
const announcements = await listActiveAnnouncements(placement)
export async function PublicAnnouncements({ placement, locale }: PublicAnnouncementsProps) {
const announcements = await listActiveAnnouncements(placement, new Date(), locale)
if (announcements.length === 0) {
return null