diff --git a/apps/admin/src/app/navigation/page.tsx b/apps/admin/src/app/navigation/page.tsx index 0ef2ff4..52db070 100644 --- a/apps/admin/src/app/navigation/page.tsx +++ b/apps/admin/src/app/navigation/page.tsx @@ -5,6 +5,7 @@ import { listNavigationMenus, listPages, updateNavigationItem, + upsertNavigationItemTranslation, } from "@cms/db" import { Button } from "@cms/ui/button" import { revalidatePath } from "next/cache" @@ -18,6 +19,9 @@ import { requirePermissionForRoute } from "@/lib/route-guards" export const dynamic = "force-dynamic" type SearchParamsInput = Record +const SUPPORTED_LOCALES = ["de", "en", "es", "fr"] as const + +type SupportedLocale = (typeof SUPPORTED_LOCALES)[number] function readFirstValue(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { @@ -53,6 +57,14 @@ function readInt(formData: FormData, field: string, fallback = 0): number { return parsed } +function normalizeLocale(input: string | null): SupportedLocale { + if (input && SUPPORTED_LOCALES.includes(input as SupportedLocale)) { + return input as SupportedLocale + } + + return "en" +} + function redirectWithState(params: { notice?: string; error?: string }) { const query = new URLSearchParams() @@ -165,6 +177,31 @@ async function deleteItemAction(formData: FormData) { redirectWithState({ notice: "Navigation item deleted." }) } +async function upsertItemTranslationAction(formData: FormData) { + "use server" + + await requirePermissionForRoute({ + nextPath: "/navigation", + permission: "navigation:write", + scope: "team", + }) + + const locale = normalizeLocale(readInputString(formData, "locale")) + + try { + await upsertNavigationItemTranslation({ + navigationItemId: readInputString(formData, "navigationItemId"), + locale, + label: readInputString(formData, "label"), + }) + } catch { + redirectWithState({ error: "Failed to save item translation." }) + } + + revalidatePath("/navigation") + redirectWithState({ notice: "Navigation item translation saved." }) +} + export default async function NavigationManagementPage({ searchParams, }: { @@ -184,6 +221,7 @@ export default async function NavigationManagementPage({ const notice = readFirstValue(resolvedSearchParams.notice) const error = readFirstValue(resolvedSearchParams.error) + const selectedLocale = normalizeLocale(readFirstValue(resolvedSearchParams.locale)) return (
+
+ {SUPPORTED_LOCALES.map((locale) => ( + + {locale.toUpperCase()} + + ))} +
+ {menus.length === 0 ? (
No navigation menus yet. @@ -238,94 +292,126 @@ export default async function NavigationManagementPage({ {menu.items.length === 0 ? (

No items in this menu.

) : ( - menu.items.map((item) => ( -
- -
- - - -
+ menu.items.map((item) => { + const translation = item.translations.find( + (entry) => entry.locale === selectedLocale, + ) -
- - -
+ return ( +
+ + +
+ + + +
-
- -
- - -
+
+ + +
+ +
+ +
+ + +
+
+ + +
+ + + +

+ Translation ({selectedLocale.toUpperCase()}) - saved locales:{" "} + {item.translations.length > 0 + ? item.translations + .map((entry) => entry.locale.toUpperCase()) + .join(", ") + : "none"} +

+ +
+ + +
+
- - )) + ) + }) )}
diff --git a/apps/admin/src/app/news/page.tsx b/apps/admin/src/app/news/page.tsx index 7edc39e..e52f6ab 100644 --- a/apps/admin/src/app/news/page.tsx +++ b/apps/admin/src/app/news/page.tsx @@ -1,4 +1,10 @@ -import { createPost, deletePost, listPosts, updatePost } from "@cms/db" +import { + createPost, + deletePost, + listPostsWithTranslations, + updatePost, + upsertPostTranslation, +} from "@cms/db" import { Button } from "@cms/ui/button" import { revalidatePath } from "next/cache" import { redirect } from "next/navigation" @@ -9,6 +15,9 @@ import { requirePermissionForRoute } from "@/lib/route-guards" export const dynamic = "force-dynamic" type SearchParamsInput = Record +const SUPPORTED_LOCALES = ["de", "en", "es", "fr"] as const + +type SupportedLocale = (typeof SUPPORTED_LOCALES)[number] function readFirstValue(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { @@ -28,6 +37,14 @@ function readNullableString(formData: FormData, field: string): string | undefin return value.length > 0 ? value : undefined } +function normalizeLocale(input: string | null): SupportedLocale { + if (input && SUPPORTED_LOCALES.includes(input as SupportedLocale)) { + return input as SupportedLocale + } + + return "en" +} + function redirectWithState(params: { notice?: string; error?: string }) { const query = new URLSearchParams() @@ -115,6 +132,34 @@ async function deleteNewsAction(formData: FormData) { redirectWithState({ notice: "Post deleted." }) } +async function upsertNewsTranslationAction(formData: FormData) { + "use server" + + await requirePermissionForRoute({ + nextPath: "/news", + permission: "news:write", + scope: "team", + }) + + const locale = normalizeLocale(readInputString(formData, "locale")) + + try { + await upsertPostTranslation({ + postId: readInputString(formData, "postId"), + locale, + title: readInputString(formData, "title"), + excerpt: readNullableString(formData, "excerpt") ?? null, + body: readInputString(formData, "body"), + }) + } catch { + redirectWithState({ error: "Failed to save translation." }) + } + + revalidatePath("/news") + revalidatePath("/") + redirectWithState({ notice: "Post translation saved." }) +} + export default async function NewsManagementPage({ searchParams, }: { @@ -126,10 +171,14 @@ export default async function NewsManagementPage({ scope: "team", }) - const [resolvedSearchParams, posts] = await Promise.all([searchParams, listPosts()]) + const [resolvedSearchParams, posts] = await Promise.all([ + searchParams, + listPostsWithTranslations(), + ]) const notice = readFirstValue(resolvedSearchParams.notice) const error = readFirstValue(resolvedSearchParams.error) + const selectedLocale = normalizeLocale(readFirstValue(resolvedSearchParams.locale)) return (
- {posts.map((post) => ( -
- -
- - -
- -