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
@@ -70,11 +70,11 @@ export default async function SoundsPage() {
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Song Disks</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Carica e gestisci i file MP3 dei song disk riprodotti dai jukebox
|
||||
in-game. I file sono serviti da <code>{baseUrl}</code> (configurabile
|
||||
in{" "}
|
||||
<a href="/admin/settings/cms" className="underline">
|
||||
Impostazioni > CMS
|
||||
Upload and manage MP3 files for song disks played by in-game
|
||||
jukeboxes. Files are served from <code>{baseUrl}</code>{" "}
|
||||
(configurable in{" "}
|
||||
<a href="/admin/settings" className="underline">
|
||||
Settings
|
||||
</a>
|
||||
).
|
||||
</p>
|
||||
|
||||
@@ -121,8 +121,8 @@ export function SoundsClient({
|
||||
if (!deleting) return;
|
||||
const id = deleting.id;
|
||||
run(() => deleteSoundtrack({ id }), {
|
||||
successMessage: `Song disk #${id} eliminato.`,
|
||||
errorMessage: `Impossibile eliminare il song disk #${id}.`,
|
||||
successMessage: `Song disk #${id} deleted.`,
|
||||
errorMessage: `Unable to delete song disk #${id}.`,
|
||||
onSuccess: () => {
|
||||
invalidateSongPickerCache();
|
||||
setDeleting(null);
|
||||
@@ -139,7 +139,7 @@ export function SoundsClient({
|
||||
<Input
|
||||
value={query}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -151,9 +151,9 @@ export function SoundsClient({
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Tutti ({soundtracks.length})</SelectItem>
|
||||
<SelectItem value="all">All ({soundtracks.length})</SelectItem>
|
||||
<SelectItem value="single">
|
||||
Singoli MP3 ({counts.single})
|
||||
Single MP3s ({counts.single})
|
||||
</SelectItem>
|
||||
<SelectItem value="composition">
|
||||
Composition ({counts.composition})
|
||||
@@ -199,15 +199,15 @@ export function SoundsClient({
|
||||
{soundtracks.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Music className="h-8 w-8 opacity-50" />
|
||||
<p>Nessun song disk caricato.</p>
|
||||
<p>No song disks uploaded yet.</p>
|
||||
{canEdit && (
|
||||
<p className="text-xs">
|
||||
Clicca “Upload MP3” per iniziare.
|
||||
Click “Upload MP3” to get started.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>Nessun risultato per i filtri attivi.</>
|
||||
<>No results for the active filters.</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -293,7 +293,7 @@ export function SoundsClient({
|
||||
className="h-8 w-8"
|
||||
onClick={() => setEditing(s)}
|
||||
disabled={isPending}
|
||||
title="Modifica"
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -303,7 +303,7 @@ export function SoundsClient({
|
||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||
onClick={() => setDeleting(s)}
|
||||
disabled={isPending}
|
||||
title="Elimina"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -345,27 +345,26 @@ export function SoundsClient({
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Eliminare “{deleting.name}”?
|
||||
Delete “{deleting.name}”?
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{deleting.usedBy > 0 ? (
|
||||
<>
|
||||
Questo song disk è attualmente usato da{" "}
|
||||
This song disk is currently used by{" "}
|
||||
<strong>{deleting.usedBy}</strong> catalog item
|
||||
{deleting.usedBy === 1 ? "" : "s"}. Eliminandolo, quei
|
||||
catalog item verranno riportati a “nessun song
|
||||
disk”.
|
||||
{deleting.usedBy === 1 ? "" : "s"}. Deleting it will reset
|
||||
those catalog items to “no song disk”.
|
||||
{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"
|
||||
? "Il file MP3 sarà rimosso dal disco e la riga dal database."
|
||||
: "La riga sarà rimossa dal database. Nessun file viene eliminato (una composition non ha MP3 dedicato)."}{" "}
|
||||
L'azione è irreversibile.
|
||||
? "The MP3 file will be removed from disk and the row from the database."
|
||||
: "The row will be removed from the database. No file is deleted (a composition has no dedicated MP3)."}{" "}
|
||||
This action cannot be undone.
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
@@ -376,7 +375,7 @@ export function SoundsClient({
|
||||
onClick={() => setDeleting(null)}
|
||||
disabled={isPending}
|
||||
>
|
||||
Annulla
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -384,7 +383,7 @@ export function SoundsClient({
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
|
||||
Elimina
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -420,11 +419,11 @@ function UploadDialog({
|
||||
e.preventDefault();
|
||||
const file = fileRef.current?.files?.[0];
|
||||
if (!file) {
|
||||
toast.error("Seleziona un file MP3.");
|
||||
toast.error("Select an MP3 file.");
|
||||
return;
|
||||
}
|
||||
if (!name.trim()) {
|
||||
toast.error("Il nome è obbligatorio.");
|
||||
toast.error("Name is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -445,16 +444,16 @@ function UploadDialog({
|
||||
id?: number;
|
||||
};
|
||||
if (!res.ok || !json.ok) {
|
||||
toast.error(json.error || "Upload fallito.");
|
||||
toast.error(json.error || "Upload failed.");
|
||||
return;
|
||||
}
|
||||
toast.success(`Song disk #${json.id} caricato.`);
|
||||
toast.success(`Song disk #${json.id} uploaded.`);
|
||||
reset();
|
||||
onOpenChange(false);
|
||||
onUploaded();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Errore di rete durante l'upload.");
|
||||
toast.error("Network error during upload.");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
@@ -471,14 +470,14 @@ function UploadDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>Upload Song Disk</DialogTitle>
|
||||
<DialogDescription>
|
||||
Carica un file MP3. Dimensione massima 10 MB. Il codice sarà
|
||||
generato automaticamente come <code>song_{id}</code>.
|
||||
Upload an MP3 file. Max size 10 MB. The code will be generated
|
||||
automatically as <code>song_{id}</code>.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="sound-file">File MP3</Label>
|
||||
<Label htmlFor="sound-file">MP3 file</Label>
|
||||
<Input
|
||||
id="sound-file"
|
||||
type="file"
|
||||
@@ -488,23 +487,23 @@ function UploadDialog({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="sound-name">Nome</Label>
|
||||
<Label htmlFor="sound-name">Name</Label>
|
||||
<Input
|
||||
id="sound-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Titolo del brano"
|
||||
placeholder="Track title"
|
||||
maxLength={100}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="sound-author">Autore</Label>
|
||||
<Label htmlFor="sound-author">Author</Label>
|
||||
<Input
|
||||
id="sound-author"
|
||||
value={author}
|
||||
onChange={(e) => setAuthor(e.target.value)}
|
||||
placeholder="Autore (opzionale)"
|
||||
placeholder="Author (optional)"
|
||||
maxLength={50}
|
||||
/>
|
||||
</div>
|
||||
@@ -516,7 +515,7 @@ function UploadDialog({
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={uploading}
|
||||
>
|
||||
Annulla
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={uploading}>
|
||||
{uploading ? (
|
||||
@@ -524,7 +523,7 @@ function UploadDialog({
|
||||
) : (
|
||||
<Upload className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
Carica
|
||||
Upload
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
@@ -555,12 +554,12 @@ function EditDialog({
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) {
|
||||
toast.error("Il nome è obbligatorio.");
|
||||
toast.error("Name is required.");
|
||||
return;
|
||||
}
|
||||
const lenNum = Number.parseInt(length, 10);
|
||||
if (!Number.isFinite(lenNum) || lenNum < 0) {
|
||||
toast.error("La durata deve essere un numero ≥ 0.");
|
||||
toast.error("Length must be a number ≥ 0.");
|
||||
return;
|
||||
}
|
||||
run(
|
||||
@@ -573,8 +572,8 @@ function EditDialog({
|
||||
length: lenNum,
|
||||
}),
|
||||
{
|
||||
successMessage: "Song disk aggiornato.",
|
||||
errorMessage: "Impossibile aggiornare il song disk.",
|
||||
successMessage: "Song disk updated.",
|
||||
errorMessage: "Unable to update the song disk.",
|
||||
onSuccess: onSaved,
|
||||
},
|
||||
);
|
||||
@@ -585,21 +584,21 @@ function EditDialog({
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Modifica {isComposition ? "Trax Composition" : "Song Disk"}
|
||||
Edit {isComposition ? "Trax Composition" : "Song Disk"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<span className="font-mono text-xs">{soundtrack.code}</span>
|
||||
{isComposition ? (
|
||||
<>
|
||||
{" "}
|
||||
— composition di {soundtrack.samples.length} sample. Modifica il
|
||||
campo <code>track</code> solo se sai cosa stai facendo.
|
||||
— composition of {soundtrack.samples.length} samples. Edit the{" "}
|
||||
<code>track</code> field only if you know what you are doing.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
— il file MP3 non può essere sostituito: elimina e ricarica se
|
||||
devi cambiare audio.
|
||||
— the MP3 file cannot be replaced: delete and re-upload if you
|
||||
need to change the audio.
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
@@ -607,7 +606,7 @@ function EditDialog({
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="edit-name">Nome</Label>
|
||||
<Label htmlFor="edit-name">Name</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
value={name}
|
||||
@@ -617,7 +616,7 @@ function EditDialog({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="edit-author">Autore</Label>
|
||||
<Label htmlFor="edit-author">Author</Label>
|
||||
<Input
|
||||
id="edit-author"
|
||||
value={author}
|
||||
@@ -626,7 +625,7 @@ function EditDialog({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="edit-length">Durata (secondi)</Label>
|
||||
<Label htmlFor="edit-length">Length (seconds)</Label>
|
||||
<Input
|
||||
id="edit-length"
|
||||
type="number"
|
||||
@@ -636,8 +635,8 @@ function EditDialog({
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{isComposition
|
||||
? "Durata totale della composition. Lascia 0 se sconosciuta."
|
||||
: "Impostato automaticamente dall’upload. Modifica solo se errato."}
|
||||
? "Total composition length. Leave 0 if unknown."
|
||||
: "Set automatically from the upload. Edit only if incorrect."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -653,8 +652,8 @@ function EditDialog({
|
||||
placeholder="1:371,8;374,4;378,4;0,4;…"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Formato: <code>canale:sample_id,durata;sample_id,durata;…</code>{" "}
|
||||
— <code>0</code> = silenzio. Sample IDs rilevati:{" "}
|
||||
Format: <code>channel:sample_id,length;sample_id,length;…</code>{" "}
|
||||
— <code>0</code> = silence. Detected sample IDs:{" "}
|
||||
<span className="font-mono">
|
||||
{soundtrack.samples.join(", ") || "—"}
|
||||
</span>
|
||||
@@ -670,11 +669,11 @@ function EditDialog({
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isPending}
|
||||
>
|
||||
Annulla
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
|
||||
Salva
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
Reference in new issue
Block a user