Files
EpicNext-Cms/src/app/admin/translations/translations-client.tsx
T

171 lines
5.4 KiB
TypeScript

"use client";
import { Download, Globe, Save } from "lucide-react";
import { useMemo, useState } from "react";
import { saveTranslations } from "@/actions/translations";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useServerAction } from "@/hooks/use-server-action";
import { flattenObject, unflattenObject } from "@/lib/translations-utils";
interface Props {
enJson: string;
itJson: string;
}
export function TranslationsClient({ enJson, itJson }: Props) {
const [en, setEn] = useState(() => flattenObject(JSON.parse(enJson)));
const [it, setIt] = useState(() => flattenObject(JSON.parse(itJson)));
const [search, setSearch] = useState("");
const [group, setGroup] = useState("");
const [activeLang, setActiveLang] = useState<"en" | "it">("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 groups = useMemo(() => {
const g = new Set<string>();
Object.keys(en).forEach((k) => {
const parts = k.split(".");
if (parts.length > 1) g.add(parts[0]);
});
return Array.from(g).sort();
}, [en]);
const filteredKeys = useMemo(() => {
return Object.keys(activeData).filter((key) => {
const matchSearch =
!search ||
key.toLowerCase().includes(search.toLowerCase()) ||
activeData[key].toLowerCase().includes(search.toLowerCase());
const matchGroup = !group || key.startsWith(`${group}.`);
return matchSearch && matchGroup;
});
}, [activeData, search, group]);
const totalPages = Math.ceil(filteredKeys.length / PER_PAGE);
const pageKeys = filteredKeys.slice((page - 1) * PER_PAGE, page * PER_PAGE);
function updateValue(key: string, value: string) {
setActiveData({ ...activeData, [key]: value });
}
function handleSave() {
run(() => saveTranslations({ locale: activeLang, data: unflattenObject(activeData) }), {
successMessage: "Translations saved!",
});
}
function handleExport() {
const blob = new Blob([JSON.stringify(unflattenObject(activeData), null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${activeLang}.json`;
a.click();
URL.revokeObjectURL(url);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold flex items-center gap-2">
<Globe className="w-6 h-6" />
CMS
</h1>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={handleExport}>
<Download className="w-4 h-4 mr-1" /> Export
</Button>
<Button size="sm" onClick={handleSave} disabled={isPending}>
<Save className="w-4 h-4 mr-1" /> {isPending ? "Saving..." : "Save"}
</Button>
</div>
</div>
<div className="flex gap-2 flex-wrap">
<div className="flex gap-1">
<Button
variant={activeLang === "en" ? "default" : "outline"}
size="sm"
onClick={() => setActiveLang("en")}
>
EN
</Button>
<Button
variant={activeLang === "it" ? "default" : "outline"}
size="sm"
onClick={() => setActiveLang("it")}
>
IT
</Button>
</div>
<Input
placeholder="Filter by key or value..."
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPage(1);
}}
className="max-w-xs"
/>
<select
className="rounded-md border bg-background px-3 py-1 text-sm"
value={group}
onChange={(e) => {
setGroup(e.target.value);
setPage(1);
}}
>
<option value="">All groups</option>
{groups.map((g) => (
<option key={g} value={g}>
{g}
</option>
))}
</select>
<span className="text-sm text-muted-foreground self-center">{filteredKeys.length} keys</span>
</div>
<div className="rounded-lg border bg-card divide-y">
{pageKeys.map((key) => (
<div key={key} className="flex items-center gap-3 px-4 py-2">
<span className="text-xs font-mono text-muted-foreground w-64 truncate flex-shrink-0">{key}</span>
<Input
value={activeData[key]}
onChange={(e) => updateValue(key, e.target.value)}
className="flex-1 h-8 text-sm"
/>
</div>
))}
</div>
{totalPages > 1 && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
Page {page} of {totalPages}
</span>
<div className="flex gap-1">
<Button variant="outline" size="sm" disabled={page <= 1} onClick={() => setPage(page - 1)}>
Prev
</Button>
<Button
variant="outline"
size="sm"
disabled={page >= totalPages}
onClick={() => setPage(page + 1)}
>
Next
</Button>
</div>
</div>
)}
</div>
);
}