Fix admin permissions bounce, prefixes APIs, and Italian UI leftovers.
Local Build and Deploy / deploy (push) Successful in 56s
Local Build and Deploy / deploy (push) Successful in 56s
Gate permissions on ACL manage + resolve super-admin from live user ranks, restore prefixes API routes, and anglicize hardcoded admin copy with nav i18n. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
0e89d03940
commit
3403d3b19f
38 files changed
+673
-208
No files matched your search
@@ -116,10 +116,14 @@ const SETTINGS_WHITELIST = new Set([
|
|||||||
"enabled",
|
"enabled",
|
||||||
"max_length",
|
"max_length",
|
||||||
"min_rank",
|
"min_rank",
|
||||||
|
"min_rank_to_buy",
|
||||||
"allow_colors",
|
"allow_colors",
|
||||||
"allow_bold",
|
"allow_bold",
|
||||||
"allow_italic",
|
"allow_italic",
|
||||||
"default_color",
|
"default_color",
|
||||||
|
"price_credits",
|
||||||
|
"price_points",
|
||||||
|
"points_type",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updatePrefixSettingsSchema = z.object({
|
const updatePrefixSettingsSchema = z.object({
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ export function NitroEditorDialog({
|
|||||||
htmlFor="dim-y"
|
htmlFor="dim-y"
|
||||||
className="text-xs text-muted-foreground"
|
className="text-xs text-muted-foreground"
|
||||||
>
|
>
|
||||||
Y (profondità)
|
Y (depth)
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="dim-y"
|
id="dim-y"
|
||||||
@@ -817,7 +817,7 @@ export function NitroEditorDialog({
|
|||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
>
|
>
|
||||||
Annulla
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
@@ -829,7 +829,7 @@ export function NitroEditorDialog({
|
|||||||
) : (
|
) : (
|
||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
Salva
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
+10
-10
@@ -109,24 +109,24 @@ function getNavGroups(t: (key: string) => string) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/admin/events",
|
href: "/admin/events",
|
||||||
label: "Events",
|
label: t("events"),
|
||||||
icon: <Calendar size={16} />,
|
icon: <Calendar size={16} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/admin/polls",
|
href: "/admin/polls",
|
||||||
label: "Polls",
|
label: t("polls"),
|
||||||
icon: <ClipboardList size={16} />,
|
icon: <ClipboardList size={16} />,
|
||||||
},
|
},
|
||||||
{ href: "/admin/banners", label: "Banners", icon: <Image size={16} /> },
|
{ href: "/admin/banners", label: t("banners"), icon: <Image size={16} /> },
|
||||||
{
|
{
|
||||||
href: "/admin/prefixes",
|
href: "/admin/prefixes",
|
||||||
label: "Prefixes",
|
label: t("prefixes"),
|
||||||
icon: <Languages size={16} />,
|
icon: <Languages size={16} />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("usersAccess"),
|
label: t("usersAndAccess"),
|
||||||
icon: <Shield size={14} />,
|
icon: <Shield size={14} />,
|
||||||
items: [
|
items: [
|
||||||
{ href: "/admin/users", label: t("users"), icon: <Users size={16} /> },
|
{ href: "/admin/users", label: t("users"), icon: <Users size={16} /> },
|
||||||
@@ -150,12 +150,12 @@ function getNavGroups(t: (key: string) => string) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/admin/online",
|
href: "/admin/online",
|
||||||
label: "Online Users",
|
label: t("onlineUsers"),
|
||||||
icon: <Users size={16} />,
|
icon: <Users size={16} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/admin/moderation",
|
href: "/admin/moderation",
|
||||||
label: "Moderation",
|
label: t("moderation"),
|
||||||
icon: <Shield size={16} />,
|
icon: <Shield size={16} />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -286,15 +286,15 @@ function getNavGroups(t: (key: string) => string) {
|
|||||||
{ href: "/admin/logs", label: t("logs"), icon: <FileText size={16} /> },
|
{ href: "/admin/logs", label: t("logs"), icon: <FileText size={16} /> },
|
||||||
{
|
{
|
||||||
href: "/admin/logs/audit",
|
href: "/admin/logs/audit",
|
||||||
label: "Audit Log",
|
label: t("auditLog"),
|
||||||
icon: <FileText size={16} />,
|
icon: <FileText size={16} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/admin/analytics",
|
href: "/admin/analytics",
|
||||||
label: "Analytics",
|
label: t("analytics"),
|
||||||
icon: <LayoutDashboard size={16} />,
|
icon: <LayoutDashboard size={16} />,
|
||||||
},
|
},
|
||||||
{ href: "/admin/devops", label: "DevOps", icon: <Monitor size={16} /> },
|
{ href: "/admin/devops", label: t("devops"), icon: <Monitor size={16} /> },
|
||||||
{
|
{
|
||||||
href: "/admin/wordfilter",
|
href: "/admin/wordfilter",
|
||||||
label: t("wordFilter"),
|
label: t("wordFilter"),
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export default async function AdminDashboard() {
|
|||||||
{t("recentStaffActivity")}
|
{t("recentStaffActivity")}
|
||||||
</h2>
|
</h2>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/commandocentrum"
|
href="/admin/logs"
|
||||||
className="text-xs font-medium text-[var(--admin-accent)] hover:text-[var(--admin-accent)] flex items-center gap-1 no-underline"
|
className="text-xs font-medium text-[var(--admin-accent)] hover:text-[var(--admin-accent)] flex items-center gap-1 no-underline"
|
||||||
>
|
>
|
||||||
{t("viewAll")} <ArrowRight size={12} />
|
{t("viewAll")} <ArrowRight size={12} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
|
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
|
||||||
import { getAdminContext } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { fetchEmulatorRankForEdit } from "@/lib/services/permission-ranks";
|
import { fetchEmulatorRankForEdit } from "@/lib/services/permission-ranks";
|
||||||
import { RankEditClient } from "./rank-edit-client";
|
import { RankEditClient } from "./rank-edit-client";
|
||||||
@@ -11,8 +11,12 @@ export default async function RankEditPage({
|
|||||||
params: Promise<{ locale: string; id: string }>;
|
params: Promise<{ locale: string; id: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { id: rawId } = await params;
|
const { id: rawId } = await params;
|
||||||
const { permissions } = await getAdminContext();
|
const { session, permissions } = await getAdminContext();
|
||||||
if (!permissions.isSuperAdmin) redirect("/admin");
|
if (
|
||||||
|
!canAccess(permissions, PERMS.PERMISSIONS_MANAGE, session.user.rank)
|
||||||
|
) {
|
||||||
|
redirect("/admin");
|
||||||
|
}
|
||||||
|
|
||||||
const rankId = parseInt(rawId, 10);
|
const rankId = parseInt(rawId, 10);
|
||||||
if (Number.isNaN(rankId)) notFound();
|
if (Number.isNaN(rankId)) notFound();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Prisma } from "@/generated/prisma/client";
|
import { Prisma } from "@/generated/prisma/client";
|
||||||
import { getAdminContext } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { fetchEmulatorRankSummaries } from "@/lib/services/permission-ranks";
|
import { fetchEmulatorRankSummaries } from "@/lib/services/permission-ranks";
|
||||||
import { PermissionsList } from "./permissions-list";
|
import { PermissionsList } from "./permissions-list";
|
||||||
@@ -8,8 +8,12 @@ import { PermissionsList } from "./permissions-list";
|
|||||||
export default async function PermissionsPage(_props: {
|
export default async function PermissionsPage(_props: {
|
||||||
params: Promise<{ locale: string }>;
|
params: Promise<{ locale: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { permissions } = await getAdminContext();
|
const { session, permissions } = await getAdminContext();
|
||||||
if (!permissions.isSuperAdmin) redirect("/admin");
|
if (
|
||||||
|
!canAccess(permissions, PERMS.PERMISSIONS_MANAGE, session.user.rank)
|
||||||
|
) {
|
||||||
|
redirect("/admin");
|
||||||
|
}
|
||||||
|
|
||||||
const ranks = await fetchEmulatorRankSummaries(prisma);
|
const ranks = await fetchEmulatorRankSummaries(prisma);
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default async function PrefixesPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Custom Prefixes</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Custom Prefixes</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Gestisci i prefissi degli utenti, la blacklist e le impostazioni
|
Manage user prefixes, blacklist and settings
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<PrefixesClient canEdit={canEdit} />
|
<PrefixesClient canEdit={canEdit} />
|
||||||
|
|||||||
@@ -60,46 +60,46 @@ interface PrefixFormData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PRESET_EFFECTS = [
|
const PRESET_EFFECTS = [
|
||||||
{ id: "", label: "Nessuno" },
|
{ id: "", label: "None" },
|
||||||
{ id: "glow", label: "✨ Bagliore" },
|
{ id: "glow", label: "✨ Glow" },
|
||||||
{ id: "shadow", label: "🌑 Ombra" },
|
{ id: "shadow", label: "🌑 Shadow" },
|
||||||
{ id: "italic", label: "𝑰 Corsivo" },
|
{ id: "italic", label: "𝑰 Italic" },
|
||||||
{ id: "outline", label: "🔲 Contorno" },
|
{ id: "outline", label: "🔲 Outline" },
|
||||||
{ id: "pulse", label: "💫 Pulsante" },
|
{ id: "pulse", label: "💫 Pulse" },
|
||||||
{ id: "bold-glow", label: "💡 Neon" },
|
{ id: "bold-glow", label: "💡 Neon" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const PRESET_ICONS = [
|
const PRESET_ICONS = [
|
||||||
{ emoji: "", label: "Nessuna" },
|
{ emoji: "", label: "None" },
|
||||||
{ emoji: "👑", label: "Corona" },
|
{ emoji: "👑", label: "Crown" },
|
||||||
{ emoji: "⭐", label: "Stella" },
|
{ emoji: "⭐", label: "Star" },
|
||||||
{ emoji: "❤️", label: "Cuore" },
|
{ emoji: "❤️", label: "Heart" },
|
||||||
{ emoji: "💎", label: "Diamante" },
|
{ emoji: "💎", label: "Diamond" },
|
||||||
{ emoji: "🔥", label: "Fuoco" },
|
{ emoji: "🔥", label: "Fire" },
|
||||||
{ emoji: "⚡", label: "Fulmine" },
|
{ emoji: "⚡", label: "Lightning" },
|
||||||
{ emoji: "🛡️", label: "Scudo" },
|
{ emoji: "🛡️", label: "Shield" },
|
||||||
{ emoji: "💀", label: "Teschio" },
|
{ emoji: "💀", label: "Skull" },
|
||||||
{ emoji: "🎵", label: "Musica" },
|
{ emoji: "🎵", label: "Music" },
|
||||||
{ emoji: "💠", label: "Gemma" },
|
{ emoji: "💠", label: "Gem" },
|
||||||
{ emoji: "⚔️", label: "Spade" },
|
{ emoji: "⚔️", label: "Swords" },
|
||||||
{ emoji: "🏆", label: "Trofeo" },
|
{ emoji: "🏆", label: "Trophy" },
|
||||||
{ emoji: "🚀", label: "Razzo" },
|
{ emoji: "🚀", label: "Rocket" },
|
||||||
{ emoji: "👻", label: "Fantasma" },
|
{ emoji: "👻", label: "Ghost" },
|
||||||
{ emoji: "🐾", label: "Zampa" },
|
{ emoji: "🐾", label: "Paw" },
|
||||||
{ emoji: "🍃", label: "Foglia" },
|
{ emoji: "🍃", label: "Leaf" },
|
||||||
{ emoji: "😎", label: "Cool" },
|
{ emoji: "😎", label: "Cool" },
|
||||||
{ emoji: "🐺", label: "Lupo" },
|
{ emoji: "🐺", label: "Wolf" },
|
||||||
{ emoji: "🦁", label: "Leone" },
|
{ emoji: "🦁", label: "Lion" },
|
||||||
{ emoji: "🐉", label: "Drago" },
|
{ emoji: "🐉", label: "Dragon" },
|
||||||
{ emoji: "🦄", label: "Unicorno" },
|
{ emoji: "🦄", label: "Unicorn" },
|
||||||
{ emoji: "🌙", label: "Luna" },
|
{ emoji: "🌙", label: "Moon" },
|
||||||
{ emoji: "☀️", label: "Sole" },
|
{ emoji: "☀️", label: "Sun" },
|
||||||
{ emoji: "❄️", label: "Neve" },
|
{ emoji: "❄️", label: "Snow" },
|
||||||
{ emoji: "🌹", label: "Rosa" },
|
{ emoji: "🌹", label: "Rose" },
|
||||||
{ emoji: "🎮", label: "Controller" },
|
{ emoji: "🎮", label: "Controller" },
|
||||||
{ emoji: "💰", label: "Soldi" },
|
{ emoji: "💰", label: "Money" },
|
||||||
{ emoji: "🔮", label: "Sfera" },
|
{ emoji: "🔮", label: "Orb" },
|
||||||
{ emoji: "🏴☠️", label: "Pirata" },
|
{ emoji: "🏴☠️", label: "Pirate" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function PrefixDialog({
|
function PrefixDialog({
|
||||||
@@ -167,7 +167,7 @@ function PrefixDialog({
|
|||||||
<div className="bg-background rounded-lg shadow-lg w-full max-w-md p-6 space-y-4">
|
<div className="bg-background rounded-lg shadow-lg w-full max-w-md p-6 space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold">
|
<h3 className="text-lg font-semibold">
|
||||||
{editPrefix ? "Modifica Prefisso" : "Aggiungi Prefisso"}
|
{editPrefix ? "Edit Prefix" : "Add Prefix"}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -213,7 +213,7 @@ function PrefixDialog({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<span className="text-sm font-medium">Username</span>
|
<span className="text-sm font-medium">Username</span>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Nome utente..."
|
placeholder="Username..."
|
||||||
value={form.username}
|
value={form.username}
|
||||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||||
/>
|
/>
|
||||||
@@ -221,10 +221,10 @@ function PrefixDialog({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<span className="text-sm font-medium">Testo</span>
|
<span className="text-sm font-medium">Text</span>
|
||||||
<Input
|
<Input
|
||||||
maxLength={15}
|
maxLength={15}
|
||||||
placeholder="Testo del prefix..."
|
placeholder="Prefix text..."
|
||||||
value={form.text}
|
value={form.text}
|
||||||
onChange={(e) => setForm({ ...form, text: e.target.value })}
|
onChange={(e) => setForm({ ...form, text: e.target.value })}
|
||||||
/>
|
/>
|
||||||
@@ -235,7 +235,7 @@ function PrefixDialog({
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Colore (HEX o multi: #FF0000,#00FF00)
|
Color (HEX or multi: #FF0000,#00FF00)
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
@@ -251,7 +251,7 @@ function PrefixDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<span className="text-sm font-medium">Icona</span>
|
<span className="text-sm font-medium">Icon</span>
|
||||||
<select
|
<select
|
||||||
className="w-full px-3 py-2 rounded border bg-background text-sm"
|
className="w-full px-3 py-2 rounded border bg-background text-sm"
|
||||||
value={form.icon}
|
value={form.icon}
|
||||||
@@ -266,7 +266,7 @@ function PrefixDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<span className="text-sm font-medium">Effetto</span>
|
<span className="text-sm font-medium">Effect</span>
|
||||||
<select
|
<select
|
||||||
className="w-full px-3 py-2 rounded border bg-background text-sm"
|
className="w-full px-3 py-2 rounded border bg-background text-sm"
|
||||||
value={form.effect}
|
value={form.effect}
|
||||||
@@ -288,13 +288,13 @@ function PrefixDialog({
|
|||||||
onChange={(e) => setForm({ ...form, active: e.target.checked })}
|
onChange={(e) => setForm({ ...form, active: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
<label className="text-sm" htmlFor="prefix-active">
|
<label className="text-sm" htmlFor="prefix-active">
|
||||||
Attivo
|
Active
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<Button variant="outline" onClick={onClose}>
|
<Button variant="outline" onClick={onClose}>
|
||||||
Annulla
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
@@ -305,7 +305,7 @@ function PrefixDialog({
|
|||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
>
|
>
|
||||||
<Save className="h-4 w-4 mr-1" />
|
<Save className="h-4 w-4 mr-1" />
|
||||||
{saving ? "Salvando..." : "Salva"}
|
{saving ? "Saving..." : "Save"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -338,6 +338,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
const params = new URLSearchParams({ page: String(page) });
|
const params = new URLSearchParams({ page: String(page) });
|
||||||
if (searchQuery) params.set("q", searchQuery);
|
if (searchQuery) params.set("q", searchQuery);
|
||||||
const res = await fetch(`/api/admin/prefixes?${params}`);
|
const res = await fetch(`/api/admin/prefixes?${params}`);
|
||||||
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
setPrefixes(data.prefixes);
|
setPrefixes(data.prefixes);
|
||||||
@@ -347,12 +348,14 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
|
|
||||||
const fetchBlacklist = useCallback(async () => {
|
const fetchBlacklist = useCallback(async () => {
|
||||||
const res = await fetch("/api/admin/prefixes/blacklist");
|
const res = await fetch("/api/admin/prefixes/blacklist");
|
||||||
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.ok) setBlacklist(data.words);
|
if (data.ok) setBlacklist(data.words);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchSettings = useCallback(async () => {
|
const fetchSettings = useCallback(async () => {
|
||||||
const res = await fetch("/api/admin/prefixes/settings");
|
const res = await fetch("/api/admin/prefixes/settings");
|
||||||
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.ok) setSettings(data.settings);
|
if (data.ok) setSettings(data.settings);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -367,9 +370,9 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
}, [fetchBlacklist, fetchSettings]);
|
}, [fetchBlacklist, fetchSettings]);
|
||||||
|
|
||||||
const handleDeletePrefix = async (id: number) => {
|
const handleDeletePrefix = async (id: number) => {
|
||||||
if (!confirm("Sei sicuro di voler eliminare questo prefisso?")) return;
|
if (!confirm("Are you sure you want to delete this prefix?")) return;
|
||||||
run(() => deletePrefixAction({ id }), {
|
run(() => deletePrefixAction({ id }), {
|
||||||
successMessage: "Prefisso eliminato!",
|
successMessage: "Prefix deleted!",
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
fetchPrefixes();
|
fetchPrefixes();
|
||||||
},
|
},
|
||||||
@@ -389,7 +392,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
active: data.active ? 1 : 0,
|
active: data.active ? 1 : 0,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
successMessage: "Prefisso aggiornato!",
|
successMessage: "Prefix updated!",
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
fetchPrefixes();
|
fetchPrefixes();
|
||||||
},
|
},
|
||||||
@@ -407,7 +410,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
active: data.active ? 1 : 0,
|
active: data.active ? 1 : 0,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
successMessage: "Prefisso creato!",
|
successMessage: "Prefix created!",
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
fetchPrefixes();
|
fetchPrefixes();
|
||||||
},
|
},
|
||||||
@@ -429,7 +432,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
const handleAddBlacklistWord = async () => {
|
const handleAddBlacklistWord = async () => {
|
||||||
if (!newWord.trim()) return;
|
if (!newWord.trim()) return;
|
||||||
run(() => addBlacklistWordAction({ word: newWord.trim() }), {
|
run(() => addBlacklistWordAction({ word: newWord.trim() }), {
|
||||||
successMessage: "Parola aggiunta alla blacklist!",
|
successMessage: "Word added to blacklist!",
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setNewWord("");
|
setNewWord("");
|
||||||
fetchBlacklist();
|
fetchBlacklist();
|
||||||
@@ -439,7 +442,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
|
|
||||||
const handleRemoveBlacklistWord = async (id: number) => {
|
const handleRemoveBlacklistWord = async (id: number) => {
|
||||||
run(() => removeBlacklistWordAction({ id }), {
|
run(() => removeBlacklistWordAction({ id }), {
|
||||||
successMessage: "Parola rimossa dalla blacklist!",
|
successMessage: "Word removed from blacklist!",
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
fetchBlacklist();
|
fetchBlacklist();
|
||||||
},
|
},
|
||||||
@@ -453,7 +456,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
settings: settings as unknown as Record<string, string>,
|
settings: settings as unknown as Record<string, string>,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
successMessage: "Impostazioni salvate!",
|
successMessage: "Settings saved!",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -469,9 +472,9 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
|
|
||||||
<Tabs defaultValue="prefixes">
|
<Tabs defaultValue="prefixes">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="prefixes">Prefissi Utenti</TabsTrigger>
|
<TabsTrigger value="prefixes">User Prefixes</TabsTrigger>
|
||||||
<TabsTrigger value="blacklist">Blacklist</TabsTrigger>
|
<TabsTrigger value="blacklist">Blacklist</TabsTrigger>
|
||||||
<TabsTrigger value="settings">Impostazioni</TabsTrigger>
|
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="prefixes" className="space-y-4">
|
<TabsContent value="prefixes" className="space-y-4">
|
||||||
@@ -480,7 +483,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
className="pl-8"
|
className="pl-8"
|
||||||
placeholder="Cerca per utente o testo..."
|
placeholder="Search by user or text..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchQuery(e.target.value);
|
setSearchQuery(e.target.value);
|
||||||
@@ -489,12 +492,12 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" onClick={fetchPrefixes}>
|
<Button variant="outline" onClick={fetchPrefixes}>
|
||||||
Cerca
|
Search
|
||||||
</Button>
|
</Button>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Button onClick={openAddDialog}>
|
<Button onClick={openAddDialog}>
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
Aggiungi
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -503,14 +506,14 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>ID</TableHead>
|
<TableHead>ID</TableHead>
|
||||||
<TableHead>Utente</TableHead>
|
<TableHead>User</TableHead>
|
||||||
<TableHead>Prefix</TableHead>
|
<TableHead>Prefix</TableHead>
|
||||||
<TableHead>Colore</TableHead>
|
<TableHead>Color</TableHead>
|
||||||
<TableHead>Icona</TableHead>
|
<TableHead>Icon</TableHead>
|
||||||
<TableHead>Effetto</TableHead>
|
<TableHead>Effect</TableHead>
|
||||||
<TableHead>Attivo</TableHead>
|
<TableHead>Active</TableHead>
|
||||||
<TableHead>Data</TableHead>
|
<TableHead>Date</TableHead>
|
||||||
{canEdit && <TableHead>Azioni</TableHead>}
|
{canEdit && <TableHead>Actions</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -580,9 +583,11 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
?.label || prefix.effect
|
?.label || prefix.effect
|
||||||
: "-"}
|
: "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{prefix.active ? "Si" : "No"}</TableCell>
|
<TableCell>{prefix.active ? "Yes" : "No"}</TableCell>
|
||||||
<TableCell className="text-xs">
|
<TableCell className="text-xs">
|
||||||
{new Date(prefix.created_at).toLocaleDateString()}
|
{prefix.created_at
|
||||||
|
? new Date(prefix.created_at).toLocaleDateString()
|
||||||
|
: "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -614,7 +619,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
className="text-center text-muted-foreground"
|
className="text-center text-muted-foreground"
|
||||||
colSpan={9}
|
colSpan={9}
|
||||||
>
|
>
|
||||||
Nessun prefisso trovato
|
No prefixes found
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
@@ -629,10 +634,10 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setPage(page - 1)}
|
onClick={() => setPage(page - 1)}
|
||||||
>
|
>
|
||||||
Precedente
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
Pagina {page} di {totalPages}
|
Page {page} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
disabled={page >= totalPages}
|
disabled={page >= totalPages}
|
||||||
@@ -640,7 +645,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setPage(page + 1)}
|
onClick={() => setPage(page + 1)}
|
||||||
>
|
>
|
||||||
Successiva
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -649,13 +654,13 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
<TabsContent value="blacklist" className="space-y-4">
|
<TabsContent value="blacklist" className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Parole Vietate</CardTitle>
|
<CardTitle>Forbidden Words</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Aggiungi parola..."
|
placeholder="Add word..."
|
||||||
value={newWord}
|
value={newWord}
|
||||||
onChange={(e) => setNewWord(e.target.value)}
|
onChange={(e) => setNewWord(e.target.value)}
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) =>
|
||||||
@@ -664,7 +669,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
/>
|
/>
|
||||||
<Button disabled={isPending} onClick={handleAddBlacklistWord}>
|
<Button disabled={isPending} onClick={handleAddBlacklistWord}>
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
Aggiungi
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -689,7 +694,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
))}
|
))}
|
||||||
{blacklist.length === 0 && (
|
{blacklist.length === 0 && (
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
Nessuna parola in blacklist
|
No blacklist words
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -700,12 +705,12 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
<TabsContent value="settings" className="space-y-4">
|
<TabsContent value="settings" className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Impostazioni Prefissi</CardTitle>
|
<CardTitle>Prefix Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<span className="text-sm font-medium">Lunghezza Massima</span>
|
<span className="text-sm font-medium">Max Length</span>
|
||||||
<Input
|
<Input
|
||||||
disabled={!canEdit}
|
disabled={!canEdit}
|
||||||
type="number"
|
type="number"
|
||||||
@@ -716,7 +721,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<span className="text-sm font-medium">Rank Minimo</span>
|
<span className="text-sm font-medium">Minimum Rank</span>
|
||||||
<Input
|
<Input
|
||||||
disabled={!canEdit}
|
disabled={!canEdit}
|
||||||
type="number"
|
type="number"
|
||||||
@@ -730,7 +735,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<span className="text-sm font-medium">Prezzo (Crediti)</span>
|
<span className="text-sm font-medium">Price (Credits)</span>
|
||||||
<Input
|
<Input
|
||||||
disabled={!canEdit}
|
disabled={!canEdit}
|
||||||
type="number"
|
type="number"
|
||||||
@@ -744,7 +749,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<span className="text-sm font-medium">Prezzo (Punti)</span>
|
<span className="text-sm font-medium">Price (Points)</span>
|
||||||
<Input
|
<Input
|
||||||
disabled={!canEdit}
|
disabled={!canEdit}
|
||||||
type="number"
|
type="number"
|
||||||
@@ -755,7 +760,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<span className="text-sm font-medium">Tipo Punti</span>
|
<span className="text-sm font-medium">Points Type</span>
|
||||||
<Input
|
<Input
|
||||||
disabled={!canEdit}
|
disabled={!canEdit}
|
||||||
type="number"
|
type="number"
|
||||||
@@ -765,14 +770,14 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
0 = Duckets, 5 = Diamonds, ecc.
|
0 = Duckets, 5 = Diamonds, etc.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Button disabled={isPending} onClick={saveSettings}>
|
<Button disabled={isPending} onClick={saveSettings}>
|
||||||
<Save className="h-4 w-4 mr-1" />
|
<Save className="h-4 w-4 mr-1" />
|
||||||
Salva Impostazioni
|
Save Settings
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ export default async function SoundsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Song Disks</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Song Disks</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Carica e gestisci i file MP3 dei song disk riprodotti dai jukebox
|
Upload and manage MP3 files for song disks played by in-game
|
||||||
in-game. I file sono serviti da <code>{baseUrl}</code> (configurabile
|
jukeboxes. Files are served from <code>{baseUrl}</code>{" "}
|
||||||
in{" "}
|
(configurable in{" "}
|
||||||
<a href="/admin/settings/cms" className="underline">
|
<a href="/admin/settings" className="underline">
|
||||||
Impostazioni > CMS
|
Settings
|
||||||
</a>
|
</a>
|
||||||
).
|
).
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ export function SoundsClient({
|
|||||||
if (!deleting) return;
|
if (!deleting) return;
|
||||||
const id = deleting.id;
|
const id = deleting.id;
|
||||||
run(() => deleteSoundtrack({ id }), {
|
run(() => deleteSoundtrack({ id }), {
|
||||||
successMessage: `Song disk #${id} eliminato.`,
|
successMessage: `Song disk #${id} deleted.`,
|
||||||
errorMessage: `Impossibile eliminare il song disk #${id}.`,
|
errorMessage: `Unable to delete song disk #${id}.`,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
invalidateSongPickerCache();
|
invalidateSongPickerCache();
|
||||||
setDeleting(null);
|
setDeleting(null);
|
||||||
@@ -139,7 +139,7 @@ export function SoundsClient({
|
|||||||
<Input
|
<Input
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Cerca per nome, autore, codice o id…"
|
placeholder="Search by name, author, code or id…"
|
||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,9 +151,9 @@ export function SoundsClient({
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Tutti ({soundtracks.length})</SelectItem>
|
<SelectItem value="all">All ({soundtracks.length})</SelectItem>
|
||||||
<SelectItem value="single">
|
<SelectItem value="single">
|
||||||
Singoli MP3 ({counts.single})
|
Single MP3s ({counts.single})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="composition">
|
<SelectItem value="composition">
|
||||||
Composition ({counts.composition})
|
Composition ({counts.composition})
|
||||||
@@ -199,15 +199,15 @@ export function SoundsClient({
|
|||||||
{soundtracks.length === 0 ? (
|
{soundtracks.length === 0 ? (
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<Music className="h-8 w-8 opacity-50" />
|
<Music className="h-8 w-8 opacity-50" />
|
||||||
<p>Nessun song disk caricato.</p>
|
<p>No song disks uploaded yet.</p>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<p className="text-xs">
|
<p className="text-xs">
|
||||||
Clicca “Upload MP3” per iniziare.
|
Click “Upload MP3” to get started.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>Nessun risultato per i filtri attivi.</>
|
<>No results for the active filters.</>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -293,7 +293,7 @@ export function SoundsClient({
|
|||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() => setEditing(s)}
|
onClick={() => setEditing(s)}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
title="Modifica"
|
title="Edit"
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -303,7 +303,7 @@ export function SoundsClient({
|
|||||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||||
onClick={() => setDeleting(s)}
|
onClick={() => setDeleting(s)}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
title="Elimina"
|
title="Delete"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -345,27 +345,26 @@ export function SoundsClient({
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Eliminare “{deleting.name}”?
|
Delete “{deleting.name}”?
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{deleting.usedBy > 0 ? (
|
{deleting.usedBy > 0 ? (
|
||||||
<>
|
<>
|
||||||
Questo song disk è attualmente usato da{" "}
|
This song disk is currently used by{" "}
|
||||||
<strong>{deleting.usedBy}</strong> catalog item
|
<strong>{deleting.usedBy}</strong> catalog item
|
||||||
{deleting.usedBy === 1 ? "" : "s"}. Eliminandolo, quei
|
{deleting.usedBy === 1 ? "" : "s"}. Deleting it will reset
|
||||||
catalog item verranno riportati a “nessun song
|
those catalog items to “no song disk”.
|
||||||
disk”.
|
|
||||||
{deleting.kind === "single" && (
|
{deleting.kind === "single" && (
|
||||||
<> Il file MP3 sarà rimosso dal disco.</>
|
<> The MP3 file will be removed from disk.</>
|
||||||
)}{" "}
|
)}{" "}
|
||||||
L'azione è irreversibile.
|
This action cannot be undone.
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{deleting.kind === "single"
|
{deleting.kind === "single"
|
||||||
? "Il file MP3 sarà rimosso dal disco e la riga dal database."
|
? "The MP3 file will be removed from disk and the row from the database."
|
||||||
: "La riga sarà rimossa dal database. Nessun file viene eliminato (una composition non ha MP3 dedicato)."}{" "}
|
: "The row will be removed from the database. No file is deleted (a composition has no dedicated MP3)."}{" "}
|
||||||
L'azione è irreversibile.
|
This action cannot be undone.
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
@@ -376,7 +375,7 @@ export function SoundsClient({
|
|||||||
onClick={() => setDeleting(null)}
|
onClick={() => setDeleting(null)}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
Annulla
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@@ -384,7 +383,7 @@ export function SoundsClient({
|
|||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
{isPending && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
|
{isPending && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
|
||||||
Elimina
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -420,11 +419,11 @@ function UploadDialog({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const file = fileRef.current?.files?.[0];
|
const file = fileRef.current?.files?.[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
toast.error("Seleziona un file MP3.");
|
toast.error("Select an MP3 file.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
toast.error("Il nome è obbligatorio.");
|
toast.error("Name is required.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,16 +444,16 @@ function UploadDialog({
|
|||||||
id?: number;
|
id?: number;
|
||||||
};
|
};
|
||||||
if (!res.ok || !json.ok) {
|
if (!res.ok || !json.ok) {
|
||||||
toast.error(json.error || "Upload fallito.");
|
toast.error(json.error || "Upload failed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast.success(`Song disk #${json.id} caricato.`);
|
toast.success(`Song disk #${json.id} uploaded.`);
|
||||||
reset();
|
reset();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onUploaded();
|
onUploaded();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error("Errore di rete durante l'upload.");
|
toast.error("Network error during upload.");
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
}
|
}
|
||||||
@@ -471,14 +470,14 @@ function UploadDialog({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Upload Song Disk</DialogTitle>
|
<DialogTitle>Upload Song Disk</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Carica un file MP3. Dimensione massima 10 MB. Il codice sarà
|
Upload an MP3 file. Max size 10 MB. The code will be generated
|
||||||
generato automaticamente come <code>song_{id}</code>.
|
automatically as <code>song_{id}</code>.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="sound-file">File MP3</Label>
|
<Label htmlFor="sound-file">MP3 file</Label>
|
||||||
<Input
|
<Input
|
||||||
id="sound-file"
|
id="sound-file"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -488,23 +487,23 @@ function UploadDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="sound-name">Nome</Label>
|
<Label htmlFor="sound-name">Name</Label>
|
||||||
<Input
|
<Input
|
||||||
id="sound-name"
|
id="sound-name"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="Titolo del brano"
|
placeholder="Track title"
|
||||||
maxLength={100}
|
maxLength={100}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="sound-author">Autore</Label>
|
<Label htmlFor="sound-author">Author</Label>
|
||||||
<Input
|
<Input
|
||||||
id="sound-author"
|
id="sound-author"
|
||||||
value={author}
|
value={author}
|
||||||
onChange={(e) => setAuthor(e.target.value)}
|
onChange={(e) => setAuthor(e.target.value)}
|
||||||
placeholder="Autore (opzionale)"
|
placeholder="Author (optional)"
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -516,7 +515,7 @@ function UploadDialog({
|
|||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
>
|
>
|
||||||
Annulla
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={uploading}>
|
<Button type="submit" disabled={uploading}>
|
||||||
{uploading ? (
|
{uploading ? (
|
||||||
@@ -524,7 +523,7 @@ function UploadDialog({
|
|||||||
) : (
|
) : (
|
||||||
<Upload className="h-4 w-4 mr-1" />
|
<Upload className="h-4 w-4 mr-1" />
|
||||||
)}
|
)}
|
||||||
Carica
|
Upload
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
@@ -555,12 +554,12 @@ function EditDialog({
|
|||||||
function handleSubmit(e: React.FormEvent) {
|
function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
toast.error("Il nome è obbligatorio.");
|
toast.error("Name is required.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lenNum = Number.parseInt(length, 10);
|
const lenNum = Number.parseInt(length, 10);
|
||||||
if (!Number.isFinite(lenNum) || lenNum < 0) {
|
if (!Number.isFinite(lenNum) || lenNum < 0) {
|
||||||
toast.error("La durata deve essere un numero ≥ 0.");
|
toast.error("Length must be a number ≥ 0.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
run(
|
run(
|
||||||
@@ -573,8 +572,8 @@ function EditDialog({
|
|||||||
length: lenNum,
|
length: lenNum,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
successMessage: "Song disk aggiornato.",
|
successMessage: "Song disk updated.",
|
||||||
errorMessage: "Impossibile aggiornare il song disk.",
|
errorMessage: "Unable to update the song disk.",
|
||||||
onSuccess: onSaved,
|
onSuccess: onSaved,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -585,21 +584,21 @@ function EditDialog({
|
|||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Modifica {isComposition ? "Trax Composition" : "Song Disk"}
|
Edit {isComposition ? "Trax Composition" : "Song Disk"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<span className="font-mono text-xs">{soundtrack.code}</span>
|
<span className="font-mono text-xs">{soundtrack.code}</span>
|
||||||
{isComposition ? (
|
{isComposition ? (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
— composition di {soundtrack.samples.length} sample. Modifica il
|
— composition of {soundtrack.samples.length} samples. Edit the{" "}
|
||||||
campo <code>track</code> solo se sai cosa stai facendo.
|
<code>track</code> field only if you know what you are doing.
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
— il file MP3 non può essere sostituito: elimina e ricarica se
|
— the MP3 file cannot be replaced: delete and re-upload if you
|
||||||
devi cambiare audio.
|
need to change the audio.
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
@@ -607,7 +606,7 @@ function EditDialog({
|
|||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="edit-name">Nome</Label>
|
<Label htmlFor="edit-name">Name</Label>
|
||||||
<Input
|
<Input
|
||||||
id="edit-name"
|
id="edit-name"
|
||||||
value={name}
|
value={name}
|
||||||
@@ -617,7 +616,7 @@ function EditDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="edit-author">Autore</Label>
|
<Label htmlFor="edit-author">Author</Label>
|
||||||
<Input
|
<Input
|
||||||
id="edit-author"
|
id="edit-author"
|
||||||
value={author}
|
value={author}
|
||||||
@@ -626,7 +625,7 @@ function EditDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="edit-length">Durata (secondi)</Label>
|
<Label htmlFor="edit-length">Length (seconds)</Label>
|
||||||
<Input
|
<Input
|
||||||
id="edit-length"
|
id="edit-length"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -636,8 +635,8 @@ function EditDialog({
|
|||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{isComposition
|
{isComposition
|
||||||
? "Durata totale della composition. Lascia 0 se sconosciuta."
|
? "Total composition length. Leave 0 if unknown."
|
||||||
: "Impostato automaticamente dall’upload. Modifica solo se errato."}
|
: "Set automatically from the upload. Edit only if incorrect."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -653,8 +652,8 @@ function EditDialog({
|
|||||||
placeholder="1:371,8;374,4;378,4;0,4;…"
|
placeholder="1:371,8;374,4;378,4;0,4;…"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Formato: <code>canale:sample_id,durata;sample_id,durata;…</code>{" "}
|
Format: <code>channel:sample_id,length;sample_id,length;…</code>{" "}
|
||||||
— <code>0</code> = silenzio. Sample IDs rilevati:{" "}
|
— <code>0</code> = silence. Detected sample IDs:{" "}
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
{soundtrack.samples.join(", ") || "—"}
|
{soundtrack.samples.join(", ") || "—"}
|
||||||
</span>
|
</span>
|
||||||
@@ -670,11 +669,11 @@ function EditDialog({
|
|||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
Annulla
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isPending}>
|
<Button type="submit" disabled={isPending}>
|
||||||
{isPending && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
|
{isPending && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
|
||||||
Salva
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -74,11 +74,11 @@ export function ClientTranslations({
|
|||||||
function handleSave() {
|
function handleSave() {
|
||||||
if (activeFile.readOnly) return;
|
if (activeFile.readOnly) return;
|
||||||
run(() => saveClientTranslations({ fileId: activeFile.id, data }), {
|
run(() => saveClientTranslations({ fileId: activeFile.id, data }), {
|
||||||
successMessage: "File salvato.",
|
successMessage: "File saved.",
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
if (result?.commentsLost) {
|
if (result?.commentsLost) {
|
||||||
toast.warning(
|
toast.warning(
|
||||||
"Alcune chiavi non corrispondevano al formato standard: file riserializzato e commenti rimossi.",
|
"Some keys did not match the standard format: file was reserialized and comments were removed.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
router.refresh();
|
router.refresh();
|
||||||
@@ -115,7 +115,7 @@ export function ClientTranslations({
|
|||||||
disabled={isPending || activeFile.readOnly || !!parseError}
|
disabled={isPending || activeFile.readOnly || !!parseError}
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4 mr-1" />{" "}
|
<Save className="w-4 h-4 mr-1" />{" "}
|
||||||
{isPending ? "Salvataggio..." : "Salva"}
|
{isPending ? "Saving..." : "Save"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,7 +144,7 @@ export function ClientTranslations({
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Cerca per chiave o valore..."
|
placeholder="Search by key or value..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearch(e.target.value);
|
setSearch(e.target.value);
|
||||||
@@ -154,7 +154,7 @@ export function ClientTranslations({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{filteredKeys.length} chiavi
|
{filteredKeys.length} keys
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ export function ClientTranslations({
|
|||||||
)}
|
)}
|
||||||
{activeFile.hasComments && !activeFile.readOnly && (
|
{activeFile.hasComments && !activeFile.readOnly && (
|
||||||
<Badge variant="secondary" className="gap-1 text-[11px]">
|
<Badge variant="secondary" className="gap-1 text-[11px]">
|
||||||
<Info className="w-3 h-3" /> commenti preservati
|
<Info className="w-3 h-3" /> comments preserved
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -189,9 +189,9 @@ export function ClientTranslations({
|
|||||||
<div className="rounded-md border border-[var(--admin-warning-border)] bg-[var(--admin-warning-subtle)] px-3 py-2 text-xs text-[var(--admin-warning)] dark:text-[var(--admin-warning)] flex items-start gap-2">
|
<div className="rounded-md border border-[var(--admin-warning-border)] bg-[var(--admin-warning-subtle)] px-3 py-2 text-xs text-[var(--admin-warning)] dark:text-[var(--admin-warning)] flex items-start gap-2">
|
||||||
<Lock className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
<Lock className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
Questo file è di sola lettura.{" "}
|
This file is read-only.{" "}
|
||||||
{activeFile.note ??
|
{activeFile.note ??
|
||||||
"Modificarlo manualmente non è supportato dal CMS."}
|
"Manual editing is not supported by the CMS."}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -200,24 +200,23 @@ export function ClientTranslations({
|
|||||||
<div className="rounded-md border border-[var(--admin-info-border)] bg-[var(--admin-info-subtle)] px-3 py-2 text-xs text-[var(--admin-info)] dark:text-[var(--admin-info)] flex items-start gap-2">
|
<div className="rounded-md border border-[var(--admin-info-border)] bg-[var(--admin-info-subtle)] px-3 py-2 text-xs text-[var(--admin-info)] dark:text-[var(--admin-info)] flex items-start gap-2">
|
||||||
<Info className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
<Info className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
I commenti vengono preservati: il salvataggio modifica solo le righe
|
Comments are preserved: saving only patches changed value lines. If
|
||||||
dei valori cambiati. Se una chiave usa un formato non standard ed è
|
a key uses a non-standard format and cannot be patched in-place, the
|
||||||
impossibile patchare in-place, il file viene riserializzato
|
file is reserialized (comments lost) and you will be warned.
|
||||||
(commenti persi) e ti avviseremo.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{parseError && (
|
{parseError && (
|
||||||
<div className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
<div className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||||
Errore di parsing: {parseError}
|
Parse error: {parseError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="rounded-lg border bg-card divide-y">
|
<div className="rounded-lg border bg-card divide-y">
|
||||||
{pageKeys.length === 0 && !parseError && (
|
{pageKeys.length === 0 && !parseError && (
|
||||||
<div className="px-4 py-6 text-sm text-muted-foreground text-center">
|
<div className="px-4 py-6 text-sm text-muted-foreground text-center">
|
||||||
Nessuna voce.
|
No entries.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{pageKeys.map((key) => (
|
{pageKeys.map((key) => (
|
||||||
@@ -241,7 +240,7 @@ export function ClientTranslations({
|
|||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Pagina {page} di {totalPages}
|
Page {page} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export default async function EmulatorTranslationsPage({
|
|||||||
<p className="text-sm text-muted-foreground">{t("subtitle")}</p>
|
<p className="text-sm text-muted-foreground">{t("subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline" size="sm">
|
<Button asChild variant="outline" size="sm">
|
||||||
<Link href="/admin/settings/emulator">
|
<Link href="/admin/emulator">
|
||||||
<ExternalLink className="w-4 h-4 mr-1" />
|
<ExternalLink className="w-4 h-4 mr-1" />
|
||||||
{t("fullSettings")}
|
{t("fullSettings")}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
interface BlacklistWord {
|
||||||
|
id: number;
|
||||||
|
word: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = withAdmin({ permission: PERMS.PREFIXES_VIEW }, async () => {
|
||||||
|
const words = await prisma.$queryRaw<BlacklistWord[]>`
|
||||||
|
SELECT id, word FROM custom_prefix_blacklist ORDER BY word ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
return apiOk({
|
||||||
|
words: words.map((w) => ({ ...w, created_at: "" })),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const POST = withAdmin(
|
||||||
|
{ permission: PERMS.PREFIXES_EDIT },
|
||||||
|
async (request) => {
|
||||||
|
const body = await request.json();
|
||||||
|
const word = typeof body.word === "string" ? body.word.trim() : "";
|
||||||
|
|
||||||
|
if (!word) return apiError("Word is required");
|
||||||
|
|
||||||
|
await prisma.$executeRaw`INSERT INTO custom_prefix_blacklist (word) VALUES (${word})`;
|
||||||
|
|
||||||
|
return apiOk();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DELETE = withAdmin(
|
||||||
|
{ permission: PERMS.PREFIXES_EDIT },
|
||||||
|
async (request) => {
|
||||||
|
const body = await request.json();
|
||||||
|
const id = Number(body.id);
|
||||||
|
|
||||||
|
if (!Number.isInteger(id) || id <= 0)
|
||||||
|
return apiError("Missing or invalid word id");
|
||||||
|
|
||||||
|
await prisma.$executeRaw`DELETE FROM custom_prefix_blacklist WHERE id = ${id}`;
|
||||||
|
|
||||||
|
return apiOk({ deleted: id });
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { Prisma } from "@/generated/prisma/client";
|
||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
interface UserPrefix {
|
||||||
|
id: number;
|
||||||
|
user_id: number;
|
||||||
|
text: string;
|
||||||
|
color: string;
|
||||||
|
icon: string;
|
||||||
|
effect: string;
|
||||||
|
active: number;
|
||||||
|
username?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = withAdmin(
|
||||||
|
{ permission: PERMS.PREFIXES_VIEW },
|
||||||
|
async (request) => {
|
||||||
|
const url = request.nextUrl;
|
||||||
|
const q = url.searchParams.get("q") || "";
|
||||||
|
const page = Math.max(1, Number(url.searchParams.get("page") || 1));
|
||||||
|
const limit = 50;
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
const whereFragment = q
|
||||||
|
? Prisma.sql`WHERE up.text LIKE ${`%${q}%`} OR u.username LIKE ${`%${q}%`}`
|
||||||
|
: Prisma.empty;
|
||||||
|
|
||||||
|
const prefixes = await prisma.$queryRaw<UserPrefix[]>(
|
||||||
|
Prisma.sql`SELECT up.id, up.user_id, up.text, up.color, up.icon, up.effect, up.active, u.username
|
||||||
|
FROM custom_prefixes up
|
||||||
|
LEFT JOIN users u ON u.id = up.user_id
|
||||||
|
${whereFragment}
|
||||||
|
ORDER BY up.id DESC
|
||||||
|
LIMIT ${limit} OFFSET ${offset}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const countResult = await prisma.$queryRaw<[{ total: bigint }]>(
|
||||||
|
Prisma.sql`SELECT COUNT(*) as total FROM custom_prefixes up
|
||||||
|
LEFT JOIN users u ON u.id = up.user_id
|
||||||
|
${whereFragment}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const total = Number(countResult[0]?.total || 0);
|
||||||
|
|
||||||
|
return apiOk({
|
||||||
|
prefixes: prefixes.map((p) => ({
|
||||||
|
...p,
|
||||||
|
icon: p.icon || "",
|
||||||
|
effect: p.effect || "",
|
||||||
|
active: Boolean(p.active),
|
||||||
|
created_at: "",
|
||||||
|
})),
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pages: Math.max(1, Math.ceil(total / limit)),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const POST = withAdmin(
|
||||||
|
{ permission: PERMS.PREFIXES_EDIT },
|
||||||
|
async (request) => {
|
||||||
|
const body = await request.json();
|
||||||
|
const { username, text, color, icon, effect, active } = body;
|
||||||
|
|
||||||
|
if (!username || !text || !color) {
|
||||||
|
return apiError("Missing required fields: username, text, color");
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await prisma.$queryRaw<{ id: number }[]>(
|
||||||
|
Prisma.sql`SELECT id FROM users WHERE username = ${username} LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!users || users.length === 0) {
|
||||||
|
return apiError("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = users[0].id;
|
||||||
|
|
||||||
|
await prisma.$executeRaw(
|
||||||
|
Prisma.sql`INSERT INTO custom_prefixes (user_id, text, color, icon, effect, active)
|
||||||
|
VALUES (${userId}, ${text}, ${color}, ${icon || ""}, ${effect || ""}, ${active ? 1 : 0})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return apiOk({ created: true });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const PUT = withAdmin(
|
||||||
|
{ permission: PERMS.PREFIXES_EDIT },
|
||||||
|
async (request) => {
|
||||||
|
const body = await request.json();
|
||||||
|
const { id, text, color, icon, effect, active } = body;
|
||||||
|
|
||||||
|
if (!id) return apiError("Missing prefix id");
|
||||||
|
|
||||||
|
await prisma.$executeRaw(
|
||||||
|
Prisma.sql`UPDATE custom_prefixes
|
||||||
|
SET text = ${text}, color = ${color}, icon = ${icon || ""}, effect = ${effect || ""}, active = ${active ? 1 : 0}
|
||||||
|
WHERE id = ${id}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return apiOk({ updated: id });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DELETE = withAdmin(
|
||||||
|
{ permission: PERMS.PREFIXES_EDIT },
|
||||||
|
async (request) => {
|
||||||
|
const body = await request.json();
|
||||||
|
const id = body.id;
|
||||||
|
|
||||||
|
if (!id) return apiError("Missing prefix id");
|
||||||
|
|
||||||
|
await prisma.$executeRaw(
|
||||||
|
Prisma.sql`DELETE FROM custom_prefixes WHERE id = ${id}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return apiOk({ deleted: id });
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
interface PrefixSetting {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VALID_KEYS = new Set([
|
||||||
|
"enabled",
|
||||||
|
"max_length",
|
||||||
|
"min_rank",
|
||||||
|
"min_rank_to_buy",
|
||||||
|
"allow_colors",
|
||||||
|
"allow_bold",
|
||||||
|
"allow_italic",
|
||||||
|
"default_color",
|
||||||
|
"price_credits",
|
||||||
|
"price_points",
|
||||||
|
"points_type",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: Record<string, string> = {
|
||||||
|
max_length: "15",
|
||||||
|
min_rank_to_buy: "1",
|
||||||
|
price_credits: "5",
|
||||||
|
price_points: "0",
|
||||||
|
points_type: "0",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GET = withAdmin({ permission: PERMS.PREFIXES_VIEW }, async () => {
|
||||||
|
const settings = await prisma.$queryRaw<PrefixSetting[]>`
|
||||||
|
SELECT \`key\`, \`value\` FROM custom_prefix_settings
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result: Record<string, string> = { ...DEFAULT_SETTINGS };
|
||||||
|
for (const s of settings) {
|
||||||
|
result[s.key] = s.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiOk({ settings: result });
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PUT = withAdmin(
|
||||||
|
{ permission: PERMS.PREFIXES_EDIT },
|
||||||
|
async (request) => {
|
||||||
|
const body = await request.json();
|
||||||
|
const settings = body.settings;
|
||||||
|
|
||||||
|
if (!settings || typeof settings !== "object") {
|
||||||
|
return apiError("Settings object is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(settings)) {
|
||||||
|
if (!VALID_KEYS.has(key)) {
|
||||||
|
return apiError(`Invalid setting key: ${key}`);
|
||||||
|
}
|
||||||
|
const strValue = String(value);
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
INSERT INTO custom_prefix_settings (\`key\`, \`value\`)
|
||||||
|
VALUES (${key}, ${strValue})
|
||||||
|
ON DUPLICATE KEY UPDATE \`value\` = ${strValue}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiOk();
|
||||||
|
},
|
||||||
|
);
|
||||||
+11
-1
@@ -104,10 +104,20 @@ const getCurrentAuthorizationState = cache(async (userId: number) =>
|
|||||||
resolveAuthorizationState(userId, {
|
resolveAuthorizationState(userId, {
|
||||||
user: prisma.user,
|
user: prisma.user,
|
||||||
highestRank: async () => {
|
highestRank: async () => {
|
||||||
|
// Prefer the highest rank actually held by a user. Unused high IDs in
|
||||||
|
// permission_ranks (common on Habbo DBs) would otherwise lock the real
|
||||||
|
// owner out of super-admin / permissions management.
|
||||||
const rows = await prisma.$queryRaw<
|
const rows = await prisma.$queryRaw<
|
||||||
{ highest_rank: number | bigint | null }[]
|
{ highest_rank: number | bigint | null }[]
|
||||||
>`
|
>`
|
||||||
SELECT MAX(id) AS highest_rank FROM permission_ranks
|
SELECT COALESCE(
|
||||||
|
(
|
||||||
|
SELECT MAX(u.\`rank\`)
|
||||||
|
FROM users u
|
||||||
|
INNER JOIN permission_ranks pr ON pr.id = u.\`rank\`
|
||||||
|
),
|
||||||
|
(SELECT MAX(id) FROM permission_ranks)
|
||||||
|
) AS highest_rank
|
||||||
`;
|
`;
|
||||||
return rows[0]?.highest_rank == null
|
return rows[0]?.highest_rank == null
|
||||||
? null
|
? null
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Кутии за писане",
|
"writeableBoxes": "Кутии за писане",
|
||||||
"photos": "Снимки",
|
"photos": "Снимки",
|
||||||
"tickets": "Билети",
|
"tickets": "Билети",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Потребители и достъп",
|
"usersAndAccess": "Потребители и достъп",
|
||||||
"users": "Потребители",
|
"users": "Потребители",
|
||||||
"bans": "забрани",
|
"bans": "забрани",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Приложения",
|
"applications": "Приложения",
|
||||||
"teams": "Екипи",
|
"teams": "Екипи",
|
||||||
"permissions": "Разрешения",
|
"permissions": "Разрешения",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "икономичност",
|
"economy": "икономичност",
|
||||||
"catalog": "Каталог",
|
"catalog": "Каталог",
|
||||||
"shop": "Магазин",
|
"shop": "Магазин",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Емулатор",
|
"emulator": "Емулатор",
|
||||||
"alerts": "Сигнали",
|
"alerts": "Сигнали",
|
||||||
"logs": "трупи",
|
"logs": "трупи",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Филтър за думи",
|
"wordFilter": "Филтър за думи",
|
||||||
"maintenance": "Поддръжка",
|
"maintenance": "Поддръжка",
|
||||||
"rooms": "Стаи",
|
"rooms": "Стаи",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Popsatelné krabice",
|
"writeableBoxes": "Popsatelné krabice",
|
||||||
"photos": "Fotografie",
|
"photos": "Fotografie",
|
||||||
"tickets": "Vstupenky",
|
"tickets": "Vstupenky",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Uživatelé a přístup",
|
"usersAndAccess": "Uživatelé a přístup",
|
||||||
"users": "Uživatelé",
|
"users": "Uživatelé",
|
||||||
"bans": "Zákazy",
|
"bans": "Zákazy",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Aplikace",
|
"applications": "Aplikace",
|
||||||
"teams": "Týmy",
|
"teams": "Týmy",
|
||||||
"permissions": "Oprávnění",
|
"permissions": "Oprávnění",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Ekonomika",
|
"economy": "Ekonomika",
|
||||||
"catalog": "Katalog",
|
"catalog": "Katalog",
|
||||||
"shop": "Obchod",
|
"shop": "Obchod",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Emulátor",
|
"emulator": "Emulátor",
|
||||||
"alerts": "Upozornění",
|
"alerts": "Upozornění",
|
||||||
"logs": "Protokoly",
|
"logs": "Protokoly",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Filtr slov",
|
"wordFilter": "Filtr slov",
|
||||||
"maintenance": "Údržba",
|
"maintenance": "Údržba",
|
||||||
"rooms": "Pokoje",
|
"rooms": "Pokoje",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Skrivbare kasser",
|
"writeableBoxes": "Skrivbare kasser",
|
||||||
"photos": "Fotos",
|
"photos": "Fotos",
|
||||||
"tickets": "Billetter",
|
"tickets": "Billetter",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Brugere og adgang",
|
"usersAndAccess": "Brugere og adgang",
|
||||||
"users": "Brugere",
|
"users": "Brugere",
|
||||||
"bans": "Forbud",
|
"bans": "Forbud",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Ansøgninger",
|
"applications": "Ansøgninger",
|
||||||
"teams": "Hold",
|
"teams": "Hold",
|
||||||
"permissions": "Tilladelser",
|
"permissions": "Tilladelser",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Økonomi",
|
"economy": "Økonomi",
|
||||||
"catalog": "Katalog",
|
"catalog": "Katalog",
|
||||||
"shop": "Butik",
|
"shop": "Butik",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Emulator",
|
"emulator": "Emulator",
|
||||||
"alerts": "Advarsler",
|
"alerts": "Advarsler",
|
||||||
"logs": "Logs",
|
"logs": "Logs",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Ord filter",
|
"wordFilter": "Ord filter",
|
||||||
"maintenance": "Vedligeholdelse",
|
"maintenance": "Vedligeholdelse",
|
||||||
"rooms": "Værelser",
|
"rooms": "Værelser",
|
||||||
|
|||||||
+15
-6
@@ -662,6 +662,10 @@
|
|||||||
"writeableBoxes": "Editierbare Boxen",
|
"writeableBoxes": "Editierbare Boxen",
|
||||||
"photos": "Fotos",
|
"photos": "Fotos",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Benutzer & Zugang",
|
"usersAndAccess": "Benutzer & Zugang",
|
||||||
"users": "Benutzer",
|
"users": "Benutzer",
|
||||||
"bans": "Banns",
|
"bans": "Banns",
|
||||||
@@ -670,6 +674,8 @@
|
|||||||
"applications": "Bewerbungen",
|
"applications": "Bewerbungen",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
"permissions": "Berechtigungen",
|
"permissions": "Berechtigungen",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Wirtschaft",
|
"economy": "Wirtschaft",
|
||||||
"catalog": "Katalog",
|
"catalog": "Katalog",
|
||||||
"shop": "Shop",
|
"shop": "Shop",
|
||||||
@@ -696,21 +702,24 @@
|
|||||||
"emulator": "Emulator",
|
"emulator": "Emulator",
|
||||||
"alerts": "Alarme",
|
"alerts": "Alarme",
|
||||||
"logs": "Logs",
|
"logs": "Logs",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Wortfilter",
|
"wordFilter": "Wortfilter",
|
||||||
"maintenance": "Wartung",
|
"maintenance": "Wartung",
|
||||||
"housekeeping": "Hausverwaltung",
|
|
||||||
"rooms": "Räume",
|
"rooms": "Räume",
|
||||||
"emailTemplates": "E-Mail-Vorlagen",
|
"emailTemplates": "E-Mail-Vorlagen",
|
||||||
"theme": "Design",
|
"theme": "Design",
|
||||||
"media": "Medien",
|
"media": "Medien",
|
||||||
"settings": "Einstellungen",
|
|
||||||
"backToSite": "Zurück zur Website",
|
|
||||||
"hotelManagementPanel": "Hotel-Verwaltungsbereich",
|
|
||||||
"rankLabel": "Rang {rank}",
|
|
||||||
"favicon": "Favicon",
|
"favicon": "Favicon",
|
||||||
"sounds": "Sounds",
|
"sounds": "Sounds",
|
||||||
"translations": "Übersetzungen",
|
"translations": "Übersetzungen",
|
||||||
"import": "Importieren"
|
"import": "Importieren",
|
||||||
|
"settings": "Einstellungen",
|
||||||
|
"backToSite": "Zurück zur Website",
|
||||||
|
"housekeeping": "Hausverwaltung",
|
||||||
|
"hotelManagementPanel": "Hotel-Verwaltungsbereich",
|
||||||
|
"rankLabel": "Rang {rank}"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Εγγράψιμα κουτιά",
|
"writeableBoxes": "Εγγράψιμα κουτιά",
|
||||||
"photos": "Φωτογραφίες",
|
"photos": "Φωτογραφίες",
|
||||||
"tickets": "Εισιτήρια",
|
"tickets": "Εισιτήρια",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Χρήστες και πρόσβαση",
|
"usersAndAccess": "Χρήστες και πρόσβαση",
|
||||||
"users": "Χρήστες",
|
"users": "Χρήστες",
|
||||||
"bans": "Απαγορεύσεις",
|
"bans": "Απαγορεύσεις",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Εφαρμογές",
|
"applications": "Εφαρμογές",
|
||||||
"teams": "Ομάδες",
|
"teams": "Ομάδες",
|
||||||
"permissions": "Άδειες",
|
"permissions": "Άδειες",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Οικονομία",
|
"economy": "Οικονομία",
|
||||||
"catalog": "Κατάλογος",
|
"catalog": "Κατάλογος",
|
||||||
"shop": "Κατάστημα",
|
"shop": "Κατάστημα",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Εξομοιωτής",
|
"emulator": "Εξομοιωτής",
|
||||||
"alerts": "Ειδοποιήσεις",
|
"alerts": "Ειδοποιήσεις",
|
||||||
"logs": "κούτσουρα",
|
"logs": "κούτσουρα",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Φίλτρο λέξεων",
|
"wordFilter": "Φίλτρο λέξεων",
|
||||||
"maintenance": "Συντήρηση",
|
"maintenance": "Συντήρηση",
|
||||||
"rooms": "Δωμάτια",
|
"rooms": "Δωμάτια",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Writeable boxes",
|
"writeableBoxes": "Writeable boxes",
|
||||||
"photos": "Photos",
|
"photos": "Photos",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Users & access",
|
"usersAndAccess": "Users & access",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"bans": "Bans",
|
"bans": "Bans",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Applications",
|
"applications": "Applications",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
"permissions": "Permissions",
|
"permissions": "Permissions",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Economy",
|
"economy": "Economy",
|
||||||
"catalog": "Catalog",
|
"catalog": "Catalog",
|
||||||
"shop": "Shop",
|
"shop": "Shop",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Emulator",
|
"emulator": "Emulator",
|
||||||
"alerts": "Alerts",
|
"alerts": "Alerts",
|
||||||
"logs": "Logs",
|
"logs": "Logs",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Word filter",
|
"wordFilter": "Word filter",
|
||||||
"maintenance": "Maintenance",
|
"maintenance": "Maintenance",
|
||||||
"rooms": "Rooms",
|
"rooms": "Rooms",
|
||||||
|
|||||||
+15
-6
@@ -662,6 +662,10 @@
|
|||||||
"writeableBoxes": "Cajas editables",
|
"writeableBoxes": "Cajas editables",
|
||||||
"photos": "Fotos",
|
"photos": "Fotos",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Usuarios y acceso",
|
"usersAndAccess": "Usuarios y acceso",
|
||||||
"users": "Usuarios",
|
"users": "Usuarios",
|
||||||
"bans": "Sanciones",
|
"bans": "Sanciones",
|
||||||
@@ -670,6 +674,8 @@
|
|||||||
"applications": "Solicitudes",
|
"applications": "Solicitudes",
|
||||||
"teams": "Equipos",
|
"teams": "Equipos",
|
||||||
"permissions": "Permisos",
|
"permissions": "Permisos",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Economía",
|
"economy": "Economía",
|
||||||
"catalog": "Catálogo",
|
"catalog": "Catálogo",
|
||||||
"shop": "Tienda",
|
"shop": "Tienda",
|
||||||
@@ -696,21 +702,24 @@
|
|||||||
"emulator": "Emulador",
|
"emulator": "Emulador",
|
||||||
"alerts": "Alertas",
|
"alerts": "Alertas",
|
||||||
"logs": "Registros",
|
"logs": "Registros",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Filtro de palabras",
|
"wordFilter": "Filtro de palabras",
|
||||||
"maintenance": "Mantenimiento",
|
"maintenance": "Mantenimiento",
|
||||||
"housekeeping": "Housekeeping",
|
|
||||||
"rooms": "Salas",
|
"rooms": "Salas",
|
||||||
"emailTemplates": "Plantillas de correo",
|
"emailTemplates": "Plantillas de correo",
|
||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"media": "Multimedia",
|
"media": "Multimedia",
|
||||||
"settings": "Ajustes",
|
|
||||||
"backToSite": "Volver al sitio",
|
|
||||||
"hotelManagementPanel": "Panel de gestión del hotel",
|
|
||||||
"rankLabel": "Rango {rank}",
|
|
||||||
"favicon": "Favicon",
|
"favicon": "Favicon",
|
||||||
"sounds": "Sonidos",
|
"sounds": "Sonidos",
|
||||||
"translations": "Traducciones",
|
"translations": "Traducciones",
|
||||||
"import": "Importar"
|
"import": "Importar",
|
||||||
|
"settings": "Ajustes",
|
||||||
|
"backToSite": "Volver al sitio",
|
||||||
|
"housekeeping": "Housekeeping",
|
||||||
|
"hotelManagementPanel": "Panel de gestión del hotel",
|
||||||
|
"rankLabel": "Rango {rank}"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Panel",
|
"title": "Panel",
|
||||||
|
|||||||
+15
-6
@@ -662,6 +662,10 @@
|
|||||||
"writeableBoxes": "Boîtes modifiables",
|
"writeableBoxes": "Boîtes modifiables",
|
||||||
"photos": "Photos",
|
"photos": "Photos",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Utilisateurs & accès",
|
"usersAndAccess": "Utilisateurs & accès",
|
||||||
"users": "Utilisateurs",
|
"users": "Utilisateurs",
|
||||||
"bans": "Bannissements",
|
"bans": "Bannissements",
|
||||||
@@ -670,6 +674,8 @@
|
|||||||
"applications": "Candidatures",
|
"applications": "Candidatures",
|
||||||
"teams": "Équipes",
|
"teams": "Équipes",
|
||||||
"permissions": "Permissions",
|
"permissions": "Permissions",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Économie",
|
"economy": "Économie",
|
||||||
"catalog": "Catalogue",
|
"catalog": "Catalogue",
|
||||||
"shop": "Boutique",
|
"shop": "Boutique",
|
||||||
@@ -696,21 +702,24 @@
|
|||||||
"emulator": "Émulateur",
|
"emulator": "Émulateur",
|
||||||
"alerts": "Alertes",
|
"alerts": "Alertes",
|
||||||
"logs": "Journaux",
|
"logs": "Journaux",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Filtre de mots",
|
"wordFilter": "Filtre de mots",
|
||||||
"maintenance": "Maintenance",
|
"maintenance": "Maintenance",
|
||||||
"housekeeping": "Gouvernance",
|
|
||||||
"rooms": "Salons",
|
"rooms": "Salons",
|
||||||
"emailTemplates": "Modèles d'e-mail",
|
"emailTemplates": "Modèles d'e-mail",
|
||||||
"theme": "Thème",
|
"theme": "Thème",
|
||||||
"media": "Médias",
|
"media": "Médias",
|
||||||
"settings": "Paramètres",
|
|
||||||
"backToSite": "Retour au site",
|
|
||||||
"hotelManagementPanel": "Panneau de gestion de l'hôtel",
|
|
||||||
"rankLabel": "Rang {rank}",
|
|
||||||
"favicon": "Favicon",
|
"favicon": "Favicon",
|
||||||
"sounds": "Sons",
|
"sounds": "Sons",
|
||||||
"translations": "Traductions",
|
"translations": "Traductions",
|
||||||
"import": "Importer"
|
"import": "Importer",
|
||||||
|
"settings": "Paramètres",
|
||||||
|
"backToSite": "Retour au site",
|
||||||
|
"housekeeping": "Gouvernance",
|
||||||
|
"hotelManagementPanel": "Panneau de gestion de l'hôtel",
|
||||||
|
"rankLabel": "Rang {rank}"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Tableau de bord",
|
"title": "Tableau de bord",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Kutije za pisanje",
|
"writeableBoxes": "Kutije za pisanje",
|
||||||
"photos": "Fotografije",
|
"photos": "Fotografije",
|
||||||
"tickets": "Ulaznice",
|
"tickets": "Ulaznice",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Korisnici i pristup",
|
"usersAndAccess": "Korisnici i pristup",
|
||||||
"users": "Korisnici",
|
"users": "Korisnici",
|
||||||
"bans": "Zabrane",
|
"bans": "Zabrane",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Prijave",
|
"applications": "Prijave",
|
||||||
"teams": "Timovi",
|
"teams": "Timovi",
|
||||||
"permissions": "Dozvole",
|
"permissions": "Dozvole",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Gospodarstvo",
|
"economy": "Gospodarstvo",
|
||||||
"catalog": "Katalog",
|
"catalog": "Katalog",
|
||||||
"shop": "Trgovina",
|
"shop": "Trgovina",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Emulator",
|
"emulator": "Emulator",
|
||||||
"alerts": "upozorenja",
|
"alerts": "upozorenja",
|
||||||
"logs": "Dnevnici",
|
"logs": "Dnevnici",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Filter riječi",
|
"wordFilter": "Filter riječi",
|
||||||
"maintenance": "Održavanje",
|
"maintenance": "Održavanje",
|
||||||
"rooms": "Sobe",
|
"rooms": "Sobe",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Írható dobozok",
|
"writeableBoxes": "Írható dobozok",
|
||||||
"photos": "Fényképek",
|
"photos": "Fényképek",
|
||||||
"tickets": "Jegyek",
|
"tickets": "Jegyek",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Felhasználók és hozzáférés",
|
"usersAndAccess": "Felhasználók és hozzáférés",
|
||||||
"users": "Felhasználók",
|
"users": "Felhasználók",
|
||||||
"bans": "Tiltások",
|
"bans": "Tiltások",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Alkalmazások",
|
"applications": "Alkalmazások",
|
||||||
"teams": "Csapatok",
|
"teams": "Csapatok",
|
||||||
"permissions": "Engedélyek",
|
"permissions": "Engedélyek",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Gazdaság",
|
"economy": "Gazdaság",
|
||||||
"catalog": "Katalógus",
|
"catalog": "Katalógus",
|
||||||
"shop": "Bolt",
|
"shop": "Bolt",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Emulátor",
|
"emulator": "Emulátor",
|
||||||
"alerts": "Figyelmeztetések",
|
"alerts": "Figyelmeztetések",
|
||||||
"logs": "Naplók",
|
"logs": "Naplók",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Szószűrő",
|
"wordFilter": "Szószűrő",
|
||||||
"maintenance": "Karbantartás",
|
"maintenance": "Karbantartás",
|
||||||
"rooms": "Szobák",
|
"rooms": "Szobák",
|
||||||
|
|||||||
@@ -662,6 +662,10 @@
|
|||||||
"writeableBoxes": "Box modificabili",
|
"writeableBoxes": "Box modificabili",
|
||||||
"photos": "Foto",
|
"photos": "Foto",
|
||||||
"tickets": "Ticket",
|
"tickets": "Ticket",
|
||||||
|
"events": "Eventi",
|
||||||
|
"polls": "Sondaggi",
|
||||||
|
"banners": "Banner",
|
||||||
|
"prefixes": "Prefissi",
|
||||||
"usersAndAccess": "Utenti e accesso",
|
"usersAndAccess": "Utenti e accesso",
|
||||||
"users": "Utenti",
|
"users": "Utenti",
|
||||||
"bans": "Ban",
|
"bans": "Ban",
|
||||||
@@ -670,6 +674,8 @@
|
|||||||
"applications": "Candidature",
|
"applications": "Candidature",
|
||||||
"teams": "Team",
|
"teams": "Team",
|
||||||
"permissions": "Permessi",
|
"permissions": "Permessi",
|
||||||
|
"onlineUsers": "Utenti online",
|
||||||
|
"moderation": "Moderazione",
|
||||||
"economy": "Economia",
|
"economy": "Economia",
|
||||||
"catalog": "Catalogo",
|
"catalog": "Catalogo",
|
||||||
"shop": "Negozio",
|
"shop": "Negozio",
|
||||||
@@ -696,6 +702,9 @@
|
|||||||
"emulator": "Emulatore",
|
"emulator": "Emulatore",
|
||||||
"alerts": "Avvisi",
|
"alerts": "Avvisi",
|
||||||
"logs": "Registri",
|
"logs": "Registri",
|
||||||
|
"auditLog": "Log di audit",
|
||||||
|
"analytics": "Analitiche",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Filtro parole",
|
"wordFilter": "Filtro parole",
|
||||||
"maintenance": "Manutenzione",
|
"maintenance": "Manutenzione",
|
||||||
"housekeeping": "Housekeeping",
|
"housekeeping": "Housekeeping",
|
||||||
|
|||||||
+21
-12
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Beschrijfbare boxes",
|
"writeableBoxes": "Beschrijfbare boxes",
|
||||||
"photos": "Foto's",
|
"photos": "Foto's",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Gebruikers & toegang",
|
"usersAndAccess": "Gebruikers & toegang",
|
||||||
"users": "Gebruikers",
|
"users": "Gebruikers",
|
||||||
"bans": "Bans",
|
"bans": "Bans",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Sollicitaties",
|
"applications": "Sollicitaties",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
"permissions": "Permissies",
|
"permissions": "Permissies",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Economie",
|
"economy": "Economie",
|
||||||
"catalog": "Catalogus",
|
"catalog": "Catalogus",
|
||||||
"shop": "Winkel",
|
"shop": "Winkel",
|
||||||
@@ -732,9 +738,11 @@
|
|||||||
"emulator": "Emulator",
|
"emulator": "Emulator",
|
||||||
"alerts": "Meldingen",
|
"alerts": "Meldingen",
|
||||||
"logs": "Logboeken",
|
"logs": "Logboeken",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Woordfilter",
|
"wordFilter": "Woordfilter",
|
||||||
"maintenance": "Onderhoud",
|
"maintenance": "Onderhoud",
|
||||||
"housekeeping": "Housekeeping",
|
|
||||||
"rooms": "Kamers",
|
"rooms": "Kamers",
|
||||||
"emailTemplates": "E-mailsjablonen",
|
"emailTemplates": "E-mailsjablonen",
|
||||||
"theme": "Thema",
|
"theme": "Thema",
|
||||||
@@ -745,6 +753,7 @@
|
|||||||
"import": "Importeren",
|
"import": "Importeren",
|
||||||
"settings": "Instellingen",
|
"settings": "Instellingen",
|
||||||
"backToSite": "Terug naar site",
|
"backToSite": "Terug naar site",
|
||||||
|
"housekeeping": "Housekeeping",
|
||||||
"hotelManagementPanel": "Hotelbeheerpaneel",
|
"hotelManagementPanel": "Hotelbeheerpaneel",
|
||||||
"rankLabel": "Rang {rank}"
|
"rankLabel": "Rang {rank}"
|
||||||
},
|
},
|
||||||
@@ -1504,7 +1513,7 @@
|
|||||||
},
|
},
|
||||||
"embedPage": {
|
"embedPage": {
|
||||||
"title": "Radio Embed",
|
"title": "Radio Embed",
|
||||||
"subtitle": "Copy-ready embed code for the live stream. Read-only \u2014 nothing here writes to the database. Configure the stream URL in Radio Settings (radio_stream_url).",
|
"subtitle": "Copy-ready embed code for the live stream. Read-only — nothing here writes to the database. Configure the stream URL in Radio Settings (radio_stream_url).",
|
||||||
"streamUrl": "Stream URL",
|
"streamUrl": "Stream URL",
|
||||||
"configured": "Configured",
|
"configured": "Configured",
|
||||||
"missing": "Missing",
|
"missing": "Missing",
|
||||||
@@ -1521,7 +1530,7 @@
|
|||||||
"audioSnippet": "Audio player snippet",
|
"audioSnippet": "Audio player snippet",
|
||||||
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
|
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
|
||||||
"iframeSnippet": "Iframe wrapper snippet",
|
"iframeSnippet": "Iframe wrapper snippet",
|
||||||
"iframeSnippetDesc": "Embeds the site\u2019s own radio player page in an iframe \u2014 keeps your styling and now-playing info. Adjust width/height to taste.",
|
"iframeSnippetDesc": "Embeds the site’s own radio player page in an iframe — keeps your styling and now-playing info. Adjust width/height to taste.",
|
||||||
"audioUnsupported": "Your browser does not support the audio element."
|
"audioUnsupported": "Your browser does not support the audio element."
|
||||||
},
|
},
|
||||||
"banners": {
|
"banners": {
|
||||||
@@ -1531,7 +1540,7 @@
|
|||||||
"addBanner": "Banner toevoegen",
|
"addBanner": "Banner toevoegen",
|
||||||
"createBanner": "Create banner",
|
"createBanner": "Create banner",
|
||||||
"deleteBanner": "Banner verwijderen",
|
"deleteBanner": "Banner verwijderen",
|
||||||
"noBanners": "No banners yet \u2014 add one above.",
|
"noBanners": "No banners yet — add one above.",
|
||||||
"existing": "Existing banners ({count})",
|
"existing": "Existing banners ({count})",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
@@ -1562,7 +1571,7 @@
|
|||||||
"description": "Description",
|
"description": "Description",
|
||||||
"createRank": "Create rank",
|
"createRank": "Create rank",
|
||||||
"existing": "Existing ranks ({count})",
|
"existing": "Existing ranks ({count})",
|
||||||
"noRanks": "No ranks yet \u2014 add one above.",
|
"noRanks": "No ranks yet — add one above.",
|
||||||
"activeLabel": "active",
|
"activeLabel": "active",
|
||||||
"inactiveLabel": "inactive",
|
"inactiveLabel": "inactive",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
@@ -1589,13 +1598,13 @@
|
|||||||
},
|
},
|
||||||
"apiKeysForm": {
|
"apiKeysForm": {
|
||||||
"title": "API-sleutels",
|
"title": "API-sleutels",
|
||||||
"subtitle": "API keys for external radio integrations (radio_api_keys). Keys are generated server-side and shown masked \u2014 copy the full value from the database when first issued.",
|
"subtitle": "API keys for external radio integrations (radio_api_keys). Keys are generated server-side and shown masked — copy the full value from the database when first issued.",
|
||||||
"dbError": "Could not load API keys (database unavailable).",
|
"dbError": "Could not load API keys (database unavailable).",
|
||||||
"totalKeys": "Total keys",
|
"totalKeys": "Total keys",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
"inactive": "Inactive",
|
"inactive": "Inactive",
|
||||||
"generateKey": "Nieuwe sleutel genereren",
|
"generateKey": "Nieuwe sleutel genereren",
|
||||||
"generateHint": "The secret key is generated automatically with cryptographic randomness \u2014 you do not enter it.",
|
"generateHint": "The secret key is generated automatically with cryptographic randomness — you do not enter it.",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"namePlaceholder": "AzureCast bridge",
|
"namePlaceholder": "AzureCast bridge",
|
||||||
"rateLimit": "Rate limit (requests/min)",
|
"rateLimit": "Rate limit (requests/min)",
|
||||||
@@ -1603,7 +1612,7 @@
|
|||||||
"allowedIpsPlaceholder": "comma-separated, blank = any",
|
"allowedIpsPlaceholder": "comma-separated, blank = any",
|
||||||
"generate": "Genereren",
|
"generate": "Genereren",
|
||||||
"existingKeys": "Existing keys ({count})",
|
"existingKeys": "Existing keys ({count})",
|
||||||
"noKeys": "No API keys yet \u2014 generate one above.",
|
"noKeys": "No API keys yet — generate one above.",
|
||||||
"colName": "Name",
|
"colName": "Name",
|
||||||
"colKey": "Sleutel",
|
"colKey": "Sleutel",
|
||||||
"colAllowedIps": "Allowed IPs",
|
"colAllowedIps": "Allowed IPs",
|
||||||
@@ -1656,12 +1665,12 @@
|
|||||||
"artist": "Artist",
|
"artist": "Artist",
|
||||||
"album": "Album",
|
"album": "Album",
|
||||||
"artworkUrl": "Artwork URL",
|
"artworkUrl": "Artwork URL",
|
||||||
"artworkUrlPlaceholder": "https://\u2026/cover.png",
|
"artworkUrlPlaceholder": "https://…/cover.png",
|
||||||
"duration": "Duration (seconds)",
|
"duration": "Duration (seconds)",
|
||||||
"durationPlaceholder": "e.g. 215",
|
"durationPlaceholder": "e.g. 215",
|
||||||
"sortOrder": "Sort order",
|
"sortOrder": "Sort order",
|
||||||
"playlist": "Playlist ({count})",
|
"playlist": "Playlist ({count})",
|
||||||
"noTracks": "No AutoDJ tracks yet \u2014 add one above.",
|
"noTracks": "No AutoDJ tracks yet — add one above.",
|
||||||
"colOrder": "Order",
|
"colOrder": "Order",
|
||||||
"colTitle": "Title",
|
"colTitle": "Title",
|
||||||
"colArtist": "Artist",
|
"colArtist": "Artist",
|
||||||
@@ -1679,7 +1688,7 @@
|
|||||||
},
|
},
|
||||||
"monitoring": {
|
"monitoring": {
|
||||||
"title": "Radiomonitoring",
|
"title": "Radiomonitoring",
|
||||||
"subtitle": "Read-only live probe of the configured radio endpoints (radio_now_playing_api_url, radio_listeners_api_url). Each request fails soft with a {timeout}s timeout \u2014 no data is written. Configure URLs under Settings.",
|
"subtitle": "Read-only live probe of the configured radio endpoints (radio_now_playing_api_url, radio_listeners_api_url). Each request fails soft with a {timeout}s timeout — no data is written. Configure URLs under Settings.",
|
||||||
"stream": "Stream",
|
"stream": "Stream",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
@@ -1694,7 +1703,7 @@
|
|||||||
"noNowPlayingHint": "no now-playing API set",
|
"noNowPlayingHint": "no now-playing API set",
|
||||||
"noListenersHint": "no listeners API set",
|
"noListenersHint": "no listeners API set",
|
||||||
"endpointDiagnostics": "Endpoint diagnostics",
|
"endpointDiagnostics": "Endpoint diagnostics",
|
||||||
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> reachable \u00b7 <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> not configured \u00b7 <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> fetch failed / timed out.",
|
"diagnosticsLegend": "<span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]\">OK</span> reachable · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]\">WARN</span> not configured · <span class=\"inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]\">ERROR</span> fetch failed / timed out.",
|
||||||
"streamUrl": "Stream URL",
|
"streamUrl": "Stream URL",
|
||||||
"unreachable": "unreachable",
|
"unreachable": "unreachable",
|
||||||
"refresh": "Vernieuwen"
|
"refresh": "Vernieuwen"
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Skrivbare bokser",
|
"writeableBoxes": "Skrivbare bokser",
|
||||||
"photos": "Bilder",
|
"photos": "Bilder",
|
||||||
"tickets": "Billetter",
|
"tickets": "Billetter",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Brukere og tilgang",
|
"usersAndAccess": "Brukere og tilgang",
|
||||||
"users": "Brukere",
|
"users": "Brukere",
|
||||||
"bans": "Forbud",
|
"bans": "Forbud",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Søknader",
|
"applications": "Søknader",
|
||||||
"teams": "Lag",
|
"teams": "Lag",
|
||||||
"permissions": "Tillatelser",
|
"permissions": "Tillatelser",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Økonomi",
|
"economy": "Økonomi",
|
||||||
"catalog": "Katalog",
|
"catalog": "Katalog",
|
||||||
"shop": "Handle",
|
"shop": "Handle",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Emulator",
|
"emulator": "Emulator",
|
||||||
"alerts": "Varsler",
|
"alerts": "Varsler",
|
||||||
"logs": "Logger",
|
"logs": "Logger",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Ordfilter",
|
"wordFilter": "Ordfilter",
|
||||||
"maintenance": "Vedlikehold",
|
"maintenance": "Vedlikehold",
|
||||||
"rooms": "Rom",
|
"rooms": "Rom",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Pudełka z możliwością zapisu",
|
"writeableBoxes": "Pudełka z możliwością zapisu",
|
||||||
"photos": "Zdjęcia",
|
"photos": "Zdjęcia",
|
||||||
"tickets": "Bilety",
|
"tickets": "Bilety",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Użytkownicy i dostęp",
|
"usersAndAccess": "Użytkownicy i dostęp",
|
||||||
"users": "Użytkownicy",
|
"users": "Użytkownicy",
|
||||||
"bans": "Zakazy",
|
"bans": "Zakazy",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Aplikacje",
|
"applications": "Aplikacje",
|
||||||
"teams": "Zespoły",
|
"teams": "Zespoły",
|
||||||
"permissions": "Uprawnienia",
|
"permissions": "Uprawnienia",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Ekonomia",
|
"economy": "Ekonomia",
|
||||||
"catalog": "Katalog",
|
"catalog": "Katalog",
|
||||||
"shop": "Sklep",
|
"shop": "Sklep",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Emulator",
|
"emulator": "Emulator",
|
||||||
"alerts": "Alerty",
|
"alerts": "Alerty",
|
||||||
"logs": "Dzienniki",
|
"logs": "Dzienniki",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Filtr słów",
|
"wordFilter": "Filtr słów",
|
||||||
"maintenance": "Konserwacja",
|
"maintenance": "Konserwacja",
|
||||||
"rooms": "Pokoje",
|
"rooms": "Pokoje",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Caixas graváveis",
|
"writeableBoxes": "Caixas graváveis",
|
||||||
"photos": "Fotos",
|
"photos": "Fotos",
|
||||||
"tickets": "Ingressos",
|
"tickets": "Ingressos",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Usuários e acesso",
|
"usersAndAccess": "Usuários e acesso",
|
||||||
"users": "Usuários",
|
"users": "Usuários",
|
||||||
"bans": "Proibições",
|
"bans": "Proibições",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Aplicativos",
|
"applications": "Aplicativos",
|
||||||
"teams": "Equipes",
|
"teams": "Equipes",
|
||||||
"permissions": "Permissões",
|
"permissions": "Permissões",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Economia",
|
"economy": "Economia",
|
||||||
"catalog": "Catálogo",
|
"catalog": "Catálogo",
|
||||||
"shop": "Loja",
|
"shop": "Loja",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Emulador",
|
"emulator": "Emulador",
|
||||||
"alerts": "Alertas",
|
"alerts": "Alertas",
|
||||||
"logs": "Registros",
|
"logs": "Registros",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Filtro de palavras",
|
"wordFilter": "Filtro de palavras",
|
||||||
"maintenance": "Manutenção",
|
"maintenance": "Manutenção",
|
||||||
"rooms": "Quartos",
|
"rooms": "Quartos",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Cutii care pot fi scrise",
|
"writeableBoxes": "Cutii care pot fi scrise",
|
||||||
"photos": "Fotografii",
|
"photos": "Fotografii",
|
||||||
"tickets": "bilete",
|
"tickets": "bilete",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Utilizatori și acces",
|
"usersAndAccess": "Utilizatori și acces",
|
||||||
"users": "Utilizatori",
|
"users": "Utilizatori",
|
||||||
"bans": "Interdicții",
|
"bans": "Interdicții",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Aplicații",
|
"applications": "Aplicații",
|
||||||
"teams": "Echipe",
|
"teams": "Echipe",
|
||||||
"permissions": "Permisiuni",
|
"permissions": "Permisiuni",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Economie",
|
"economy": "Economie",
|
||||||
"catalog": "Catalog",
|
"catalog": "Catalog",
|
||||||
"shop": "Magazin",
|
"shop": "Magazin",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Emulator",
|
"emulator": "Emulator",
|
||||||
"alerts": "Alerte",
|
"alerts": "Alerte",
|
||||||
"logs": "Bușteni",
|
"logs": "Bușteni",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Filtru de cuvinte",
|
"wordFilter": "Filtru de cuvinte",
|
||||||
"maintenance": "Întreținere",
|
"maintenance": "Întreținere",
|
||||||
"rooms": "Camerele",
|
"rooms": "Camerele",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Ящики для записи",
|
"writeableBoxes": "Ящики для записи",
|
||||||
"photos": "Фото",
|
"photos": "Фото",
|
||||||
"tickets": "Билеты",
|
"tickets": "Билеты",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Пользователи и доступ",
|
"usersAndAccess": "Пользователи и доступ",
|
||||||
"users": "Пользователи",
|
"users": "Пользователи",
|
||||||
"bans": "Запреты",
|
"bans": "Запреты",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Области применения",
|
"applications": "Области применения",
|
||||||
"teams": "Команды",
|
"teams": "Команды",
|
||||||
"permissions": "Разрешения",
|
"permissions": "Разрешения",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Экономика",
|
"economy": "Экономика",
|
||||||
"catalog": "Каталог",
|
"catalog": "Каталог",
|
||||||
"shop": "Магазин",
|
"shop": "Магазин",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Эмулятор",
|
"emulator": "Эмулятор",
|
||||||
"alerts": "Оповещения",
|
"alerts": "Оповещения",
|
||||||
"logs": "Журналы",
|
"logs": "Журналы",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Фильтр слов",
|
"wordFilter": "Фильтр слов",
|
||||||
"maintenance": "Техническое обслуживание",
|
"maintenance": "Техническое обслуживание",
|
||||||
"rooms": "Номера",
|
"rooms": "Номера",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Popisovateľné boxy",
|
"writeableBoxes": "Popisovateľné boxy",
|
||||||
"photos": "Fotografie",
|
"photos": "Fotografie",
|
||||||
"tickets": "Vstupenky",
|
"tickets": "Vstupenky",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Používatelia a prístup",
|
"usersAndAccess": "Používatelia a prístup",
|
||||||
"users": "Používatelia",
|
"users": "Používatelia",
|
||||||
"bans": "Zákazy",
|
"bans": "Zákazy",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Aplikácie",
|
"applications": "Aplikácie",
|
||||||
"teams": "Tímy",
|
"teams": "Tímy",
|
||||||
"permissions": "Povolenia",
|
"permissions": "Povolenia",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "ekonomika",
|
"economy": "ekonomika",
|
||||||
"catalog": "Katalóg",
|
"catalog": "Katalóg",
|
||||||
"shop": "Obchod",
|
"shop": "Obchod",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Emulátor",
|
"emulator": "Emulátor",
|
||||||
"alerts": "Upozornenia",
|
"alerts": "Upozornenia",
|
||||||
"logs": "Denníky",
|
"logs": "Denníky",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Filter slov",
|
"wordFilter": "Filter slov",
|
||||||
"maintenance": "Údržba",
|
"maintenance": "Údržba",
|
||||||
"rooms": "Izby",
|
"rooms": "Izby",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Кутије за писање",
|
"writeableBoxes": "Кутије за писање",
|
||||||
"photos": "Фотографије",
|
"photos": "Фотографије",
|
||||||
"tickets": "Улазнице",
|
"tickets": "Улазнице",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Корисници и приступ",
|
"usersAndAccess": "Корисници и приступ",
|
||||||
"users": "Корисници",
|
"users": "Корисници",
|
||||||
"bans": "Банс",
|
"bans": "Банс",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Апликације",
|
"applications": "Апликације",
|
||||||
"teams": "Тимови",
|
"teams": "Тимови",
|
||||||
"permissions": "Дозволе",
|
"permissions": "Дозволе",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Економија",
|
"economy": "Економија",
|
||||||
"catalog": "Каталог",
|
"catalog": "Каталог",
|
||||||
"shop": "Схоп",
|
"shop": "Схоп",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Емулатор",
|
"emulator": "Емулатор",
|
||||||
"alerts": "Алертс",
|
"alerts": "Алертс",
|
||||||
"logs": "Дневници",
|
"logs": "Дневници",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Филтер речи",
|
"wordFilter": "Филтер речи",
|
||||||
"maintenance": "Одржавање",
|
"maintenance": "Одржавање",
|
||||||
"rooms": "Собе",
|
"rooms": "Собе",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Skrivbara lådor",
|
"writeableBoxes": "Skrivbara lådor",
|
||||||
"photos": "Foton",
|
"photos": "Foton",
|
||||||
"tickets": "Biljetter",
|
"tickets": "Biljetter",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Användare och åtkomst",
|
"usersAndAccess": "Användare och åtkomst",
|
||||||
"users": "Användare",
|
"users": "Användare",
|
||||||
"bans": "Förbud",
|
"bans": "Förbud",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Ansökningar",
|
"applications": "Ansökningar",
|
||||||
"teams": "Lag",
|
"teams": "Lag",
|
||||||
"permissions": "Behörigheter",
|
"permissions": "Behörigheter",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Ekonomi",
|
"economy": "Ekonomi",
|
||||||
"catalog": "Katalog",
|
"catalog": "Katalog",
|
||||||
"shop": "Handla",
|
"shop": "Handla",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Emulator",
|
"emulator": "Emulator",
|
||||||
"alerts": "Varningar",
|
"alerts": "Varningar",
|
||||||
"logs": "Loggar",
|
"logs": "Loggar",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Ordfilter",
|
"wordFilter": "Ordfilter",
|
||||||
"maintenance": "Underhåll",
|
"maintenance": "Underhåll",
|
||||||
"rooms": "Rum",
|
"rooms": "Rum",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Yazılabilir kutular",
|
"writeableBoxes": "Yazılabilir kutular",
|
||||||
"photos": "Fotoğraflar",
|
"photos": "Fotoğraflar",
|
||||||
"tickets": "Biletler",
|
"tickets": "Biletler",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Kullanıcılar ve erişim",
|
"usersAndAccess": "Kullanıcılar ve erişim",
|
||||||
"users": "Kullanıcılar",
|
"users": "Kullanıcılar",
|
||||||
"bans": "Yasaklar",
|
"bans": "Yasaklar",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Uygulamalar",
|
"applications": "Uygulamalar",
|
||||||
"teams": "Takımlar",
|
"teams": "Takımlar",
|
||||||
"permissions": "İzinler",
|
"permissions": "İzinler",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Ekonomi",
|
"economy": "Ekonomi",
|
||||||
"catalog": "Katalog",
|
"catalog": "Katalog",
|
||||||
"shop": "Mağaza",
|
"shop": "Mağaza",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Emülatör",
|
"emulator": "Emülatör",
|
||||||
"alerts": "Uyarılar",
|
"alerts": "Uyarılar",
|
||||||
"logs": "Günlükler",
|
"logs": "Günlükler",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Kelime filtresi",
|
"wordFilter": "Kelime filtresi",
|
||||||
"maintenance": "Bakım",
|
"maintenance": "Bakım",
|
||||||
"rooms": "Odalar",
|
"rooms": "Odalar",
|
||||||
|
|||||||
@@ -698,6 +698,10 @@
|
|||||||
"writeableBoxes": "Ящики для запису",
|
"writeableBoxes": "Ящики для запису",
|
||||||
"photos": "Фотографії",
|
"photos": "Фотографії",
|
||||||
"tickets": "квитки",
|
"tickets": "квитки",
|
||||||
|
"events": "Events",
|
||||||
|
"polls": "Polls",
|
||||||
|
"banners": "Banners",
|
||||||
|
"prefixes": "Prefixes",
|
||||||
"usersAndAccess": "Користувачі та доступ",
|
"usersAndAccess": "Користувачі та доступ",
|
||||||
"users": "Користувачі",
|
"users": "Користувачі",
|
||||||
"bans": "Заборони",
|
"bans": "Заборони",
|
||||||
@@ -706,6 +710,8 @@
|
|||||||
"applications": "Додатки",
|
"applications": "Додатки",
|
||||||
"teams": "Команди",
|
"teams": "Команди",
|
||||||
"permissions": "Дозволи",
|
"permissions": "Дозволи",
|
||||||
|
"onlineUsers": "Online users",
|
||||||
|
"moderation": "Moderation",
|
||||||
"economy": "Економіка",
|
"economy": "Економіка",
|
||||||
"catalog": "Каталог",
|
"catalog": "Каталог",
|
||||||
"shop": "Магазин",
|
"shop": "Магазин",
|
||||||
@@ -732,6 +738,9 @@
|
|||||||
"emulator": "Емулятор",
|
"emulator": "Емулятор",
|
||||||
"alerts": "Оповіщення",
|
"alerts": "Оповіщення",
|
||||||
"logs": "Журнали",
|
"logs": "Журнали",
|
||||||
|
"auditLog": "Audit log",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"devops": "DevOps",
|
||||||
"wordFilter": "Фільтр Word",
|
"wordFilter": "Фільтр Word",
|
||||||
"maintenance": "Технічне обслуговування",
|
"maintenance": "Технічне обслуговування",
|
||||||
"rooms": "Кімнати",
|
"rooms": "Кімнати",
|
||||||
|
|||||||
Reference in new issue
Block a user