diff --git a/src/actions/translations.ts b/src/actions/translations.ts index 9f15d916..3a32e910 100644 --- a/src/actions/translations.ts +++ b/src/actions/translations.ts @@ -10,7 +10,7 @@ import { adminAction } from "@/lib/safe-action"; import { ActionError, actionOk } from "@/lib/safe-action-shared"; const saveTranslationsSchema = z.object({ - locale: z.enum(["en", "it"]), + locale: z.enum(["en", "it", "nl", "de", "fr", "es"]), data: z.record(z.string(), z.unknown()), }); diff --git a/src/app/admin/import/page.tsx b/src/app/admin/import/page.tsx new file mode 100644 index 00000000..dab2153f --- /dev/null +++ b/src/app/admin/import/page.tsx @@ -0,0 +1,50 @@ +import { BadgeCheck, Cat, Copy, Dumbbell, Puzzle, Shirt, Wrench } from "lucide-react"; +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; + +const SECTIONS = [ + { href: "/admin/import/badges", label: "Badges", icon: BadgeCheck, description: "Import and manage badge assets from external sources." }, + { href: "/admin/import/furni", label: "Furni", icon: Puzzle, description: "Import furniture definitions, SWF files, and icons." }, + { href: "/admin/import/clothing", label: "Clothing", icon: Shirt, description: "Import clothing and wearables for avatars." }, + { href: "/admin/import/effects", label: "Effects", icon: Dumbbell, description: "Import avatar effects and animations." }, + { href: "/admin/import/pets", label: "Pets", icon: Cat, description: "Import pet data and icons." }, + { href: "/admin/import/clone", label: "Clone", icon: Copy, description: "Clone existing items from one source to another." }, + { href: "/admin/import/repair", label: "Repair", icon: Wrench, description: "Audit and repair imported assets." }, +] as const; + +export default async function ImportPage() { + const { session, permissions } = await getAdminContext(); + if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { + redirect("/admin"); + } + + return ( +
+
+

Import

+

Import assets from external sources into your hotel.

+
+ +
+ {SECTIONS.map(({ href, label, icon: Icon, description }) => ( + +
+
+ +
+
+

{label}

+

{description}

+
+
+ + ))} +
+
+ ); +} diff --git a/src/app/admin/translations/cms/page.tsx b/src/app/admin/translations/cms/page.tsx index 0a2f4de7..101d30fb 100644 --- a/src/app/admin/translations/cms/page.tsx +++ b/src/app/admin/translations/cms/page.tsx @@ -2,10 +2,17 @@ import fs from "node:fs/promises"; import path from "node:path"; import { TranslationsClient } from "../translations-client"; +const LOCALES = ["en", "it", "nl", "de", "fr", "es"] as const; + export default async function CmsTranslationsPage() { const messagesDir = path.join(process.cwd(), "messages"); - const enContent = await fs.readFile(path.join(messagesDir, "en.json"), "utf-8"); - const itContent = await fs.readFile(path.join(messagesDir, "it.json"), "utf-8"); - return ; + const contents: Record = {}; + await Promise.all( + LOCALES.map(async (locale) => { + contents[locale] = await fs.readFile(path.join(messagesDir, `${locale}.json`), "utf-8"); + }), + ); + + return ; } diff --git a/src/app/admin/translations/translations-client.tsx b/src/app/admin/translations/translations-client.tsx index 0a7ae74a..fe36a5c9 100644 --- a/src/app/admin/translations/translations-client.tsx +++ b/src/app/admin/translations/translations-client.tsx @@ -8,34 +8,49 @@ import { Input } from "@/components/ui/input"; import { useServerAction } from "@/hooks/use-server-action"; import { flattenObject, unflattenObject } from "@/lib/translations-utils"; +type Locale = "en" | "it" | "nl" | "de" | "fr" | "es"; + +const LOCALES: { code: Locale; label: string }[] = [ + { code: "en", label: "EN" }, + { code: "it", label: "IT" }, + { code: "nl", label: "NL" }, + { code: "de", label: "DE" }, + { code: "fr", label: "FR" }, + { code: "es", label: "ES" }, +]; + interface Props { - enJson: string; - itJson: string; + localeContents: Record; } -export function TranslationsClient({ enJson, itJson }: Props) { - const [en, setEn] = useState(() => flattenObject(JSON.parse(enJson))); - const [it, setIt] = useState(() => flattenObject(JSON.parse(itJson))); +export function TranslationsClient({ localeContents }: Props) { + const [data, setData] = useState>>(() => { + const initial: Record> = {}; + for (const [locale, content] of Object.entries(localeContents)) { + initial[locale] = flattenObject(JSON.parse(content)); + } + return initial; + }); const [search, setSearch] = useState(""); const [group, setGroup] = useState(""); - const [activeLang, setActiveLang] = useState<"en" | "it">("en"); + const [activeLang, setActiveLang] = useState("en"); const { run, isPending } = useServerAction(); const PER_PAGE = 30; const [page, setPage] = useState(1); - const activeData = activeLang === "en" ? en : it; - const setActiveData = activeLang === "en" ? setEn : setIt; + const activeData = data[activeLang]; const groups = useMemo(() => { const g = new Set(); - Object.keys(en).forEach((k) => { + Object.keys(data["en"] ?? {}).forEach((k) => { const parts = k.split("."); if (parts.length > 1) g.add(parts[0]); }); return Array.from(g).sort(); - }, [en]); + }, [data]); const filteredKeys = useMemo(() => { + if (!activeData) return []; return Object.keys(activeData).filter((key) => { const matchSearch = !search || @@ -50,7 +65,7 @@ export function TranslationsClient({ enJson, itJson }: Props) { const pageKeys = filteredKeys.slice((page - 1) * PER_PAGE, page * PER_PAGE); function updateValue(key: string, value: string) { - setActiveData({ ...activeData, [key]: value }); + setData({ ...data, [activeLang]: { ...activeData, [key]: value } }); } function handleSave() { @@ -90,20 +105,19 @@ export function TranslationsClient({ enJson, itJson }: Props) {
- - + {LOCALES.map(({ code, label }) => ( + + ))}