revert fb8e77bb68
Local Build and Deploy / deploy (push) Successful in 1m11s

revert style: clean up code with prettier and eslint
This commit is contained in:
remco committed 2026-07-12 21:02:03 +02:00
1 parent 27de078f54
commit e85e4d74ea
378 files changed
+20710 -22428

No files matched your search

+184 -151
View File
@@ -1,12 +1,12 @@
"use client";
'use client'
import { Layers, Loader2, Music, Pencil, Plus, Search, Trash2, Upload } from "lucide-react";
import { useRouter } from "next/navigation";
import { useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { deleteSoundtrack, updateSoundtrack } from "@/actions/soundtracks";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Layers, Loader2, Music, Pencil, Plus, Search, Trash2, Upload } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'
import { deleteSoundtrack, updateSoundtrack } from '@/actions/soundtracks'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
@@ -14,40 +14,53 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import { useServerAction } from "@/hooks/use-server-action";
import { invalidateSongPickerCache } from "@/lib/client-cache/song-picker-cache";
import { parseTraxChannels } from "@/lib/trax-format";
import { TraxPlayer } from "./trax-player";
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Textarea } from '@/components/ui/textarea'
import { useServerAction } from '@/hooks/use-server-action'
import { invalidateSongPickerCache } from '@/lib/client-cache/song-picker-cache'
import { parseTraxChannels } from '@/lib/trax-format'
import { TraxPlayer } from './trax-player'
interface SoundtrackRow {
id: number;
code: string;
name: string;
author: string;
length: number;
track: string;
samples: number[];
kind: "single" | "composition";
usedBy: number;
id: number
code: string
name: string
author: string
length: number
track: string
samples: number[]
kind: 'single' | 'composition'
usedBy: number
}
function formatLength(seconds: number): string {
if (!seconds || seconds <= 0) return "—";
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m}:${String(s).padStart(2, "0")}`;
if (!seconds || seconds <= 0) return '—'
const m = Math.floor(seconds / 60)
const s = seconds % 60
return `${m}:${String(s).padStart(2, '0')}`
}
function formatSamples(samples: number[]): string {
if (samples.length === 0) return "—";
if (samples.length <= 20) return samples.join(", ");
return `${samples.slice(0, 20).join(", ")}, …(+${samples.length - 20})`;
if (samples.length === 0) return '—'
if (samples.length <= 20) return samples.join(', ')
return `${samples.slice(0, 20).join(', ')}, …(+${samples.length - 20})`
}
export function SoundsClient({
@@ -55,56 +68,56 @@ export function SoundsClient({
canEdit,
baseUrl,
}: {
soundtracks: SoundtrackRow[];
canEdit: boolean;
baseUrl: string;
soundtracks: SoundtrackRow[]
canEdit: boolean
baseUrl: string
}) {
const router = useRouter();
const { run, isPending } = useServerAction();
const router = useRouter()
const { run, isPending } = useServerAction()
const [query, setQuery] = useState("");
const [kindFilter, setKindFilter] = useState<"all" | "single" | "composition">("all");
const [uploadOpen, setUploadOpen] = useState(false);
const [editing, setEditing] = useState<SoundtrackRow | null>(null);
const [deleting, setDeleting] = useState<SoundtrackRow | null>(null);
const [playingId, setPlayingId] = useState<number | null>(null);
const [query, setQuery] = useState('')
const [kindFilter, setKindFilter] = useState<'all' | 'single' | 'composition'>('all')
const [uploadOpen, setUploadOpen] = useState(false)
const [editing, setEditing] = useState<SoundtrackRow | null>(null)
const [deleting, setDeleting] = useState<SoundtrackRow | null>(null)
const [playingId, setPlayingId] = useState<number | null>(null)
const counts = useMemo(() => {
let single = 0;
let composition = 0;
let single = 0
let composition = 0
for (const s of soundtracks) {
if (s.kind === "composition") composition++;
else single++;
if (s.kind === 'composition') composition++
else single++
}
return { single, composition };
}, [soundtracks]);
return { single, composition }
}, [soundtracks])
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
const q = query.trim().toLowerCase()
return soundtracks.filter((s) => {
if (kindFilter !== "all" && s.kind !== kindFilter) return false;
if (!q) return true;
if (kindFilter !== 'all' && s.kind !== kindFilter) return false
if (!q) return true
return (
s.name.toLowerCase().includes(q) ||
s.author.toLowerCase().includes(q) ||
s.code.toLowerCase().includes(q) ||
String(s.id).includes(q)
);
});
}, [soundtracks, query, kindFilter]);
)
})
}, [soundtracks, query, kindFilter])
function handleDelete() {
if (!deleting) return;
const id = deleting.id;
if (!deleting) return
const id = deleting.id
run(() => deleteSoundtrack({ id }), {
successMessage: `Song disk #${id} eliminato.`,
errorMessage: `Impossibile eliminare il song disk #${id}.`,
onSuccess: () => {
invalidateSongPickerCache();
setDeleting(null);
router.refresh();
invalidateSongPickerCache()
setDeleting(null)
router.refresh()
},
});
})
}
return (
@@ -159,12 +172,17 @@ export function SoundsClient({
<TableBody>
{filtered.length === 0 ? (
<TableRow>
<TableCell colSpan={canEdit ? 9 : 8} className="text-center text-muted-foreground py-8">
<TableCell
colSpan={canEdit ? 9 : 8}
className="text-center text-muted-foreground py-8"
>
{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>
{canEdit && <p className="text-xs">Clicca &ldquo;Upload MP3&rdquo; per iniziare.</p>}
{canEdit && (
<p className="text-xs">Clicca &ldquo;Upload MP3&rdquo; per iniziare.</p>
)}
</div>
) : (
<>Nessun risultato per i filtri attivi.</>
@@ -176,7 +194,7 @@ export function SoundsClient({
<TableRow key={s.id}>
<TableCell className="font-mono text-xs">{s.id}</TableCell>
<TableCell>
{s.kind === "composition" ? (
{s.kind === 'composition' ? (
<Badge variant="outline" className="gap-1">
<Layers className="h-3 w-3" />
Trax
@@ -190,7 +208,7 @@ export function SoundsClient({
</TableCell>
<TableCell className="font-mono text-xs break-all">{s.code}</TableCell>
<TableCell className="font-medium">{s.name}</TableCell>
<TableCell className="text-muted-foreground">{s.author || "—"}</TableCell>
<TableCell className="text-muted-foreground">{s.author || '—'}</TableCell>
<TableCell className="font-mono text-xs">{formatLength(s.length)}</TableCell>
<TableCell>
{s.usedBy > 0 ? (
@@ -200,7 +218,7 @@ export function SoundsClient({
)}
</TableCell>
<TableCell>
{s.kind === "composition" ? (
{s.kind === 'composition' ? (
<div className="space-y-1 max-w-[300px]">
<TraxPlayer
channels={parseTraxChannels(s.track)}
@@ -222,7 +240,9 @@ export function SoundsClient({
src={`${baseUrl}${encodeURIComponent(s.code)}.mp3`}
className="h-8 max-w-[280px]"
onEnded={() => setPlayingId(null)}
onError={() => toast.error(`Impossibile riprodurre "${s.code}.mp3" da ${baseUrl}.`)}
onError={() =>
toast.error(`Impossibile riprodurre "${s.code}.mp3" da ${baseUrl}.`)
}
/>
) : (
<Button size="sm" variant="ghost" onClick={() => setPlayingId(s.id)}>
@@ -268,8 +288,8 @@ export function SoundsClient({
open={uploadOpen}
onOpenChange={setUploadOpen}
onUploaded={() => {
invalidateSongPickerCache();
router.refresh();
invalidateSongPickerCache()
router.refresh()
}}
/>
)}
@@ -279,9 +299,9 @@ export function SoundsClient({
soundtrack={editing}
onOpenChange={(open) => !open && setEditing(null)}
onSaved={() => {
invalidateSongPickerCache();
setEditing(null);
router.refresh();
invalidateSongPickerCache()
setEditing(null)
router.refresh()
}}
/>
)}
@@ -294,17 +314,17 @@ export function SoundsClient({
<DialogDescription>
{deleting.usedBy > 0 ? (
<>
Questo song disk è attualmente usato da <strong>{deleting.usedBy}</strong> catalog item
{deleting.usedBy === 1 ? "" : "s"}. Eliminandolo, quei catalog item verranno riportati a
&ldquo;nessun song disk&rdquo;.
{deleting.kind === "single" && <> Il file MP3 sarà rimosso dal disco.</>} L&apos;azione è
irreversibile.
Questo song disk è attualmente usato da <strong>{deleting.usedBy}</strong>{' '}
catalog item{deleting.usedBy === 1 ? '' : 's'}. Eliminandolo, quei catalog item
verranno riportati a &ldquo;nessun song disk&rdquo;.
{deleting.kind === 'single' && <> Il file MP3 sarà rimosso dal disco.</>}{' '}
L&apos;azione è irreversibile.
</>
) : (
<>
{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)."}{" "}
{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&apos;azione è irreversibile.
</>
)}
@@ -323,7 +343,7 @@ export function SoundsClient({
</Dialog>
)}
</div>
);
)
}
// ── Upload dialog ────────────────────────────────────────────────────
@@ -333,55 +353,55 @@ function UploadDialog({
onOpenChange,
onUploaded,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
onUploaded: () => void;
open: boolean
onOpenChange: (open: boolean) => void
onUploaded: () => void
}) {
const fileRef = useRef<HTMLInputElement>(null);
const [name, setName] = useState("");
const [author, setAuthor] = useState("");
const [uploading, setUploading] = useState(false);
const fileRef = useRef<HTMLInputElement>(null)
const [name, setName] = useState('')
const [author, setAuthor] = useState('')
const [uploading, setUploading] = useState(false)
function reset() {
setName("");
setAuthor("");
if (fileRef.current) fileRef.current.value = "";
setName('')
setAuthor('')
if (fileRef.current) fileRef.current.value = ''
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const file = fileRef.current?.files?.[0];
e.preventDefault()
const file = fileRef.current?.files?.[0]
if (!file) {
toast.error("Seleziona un file MP3.");
return;
toast.error('Seleziona un file MP3.')
return
}
if (!name.trim()) {
toast.error("Il nome è obbligatorio.");
return;
toast.error('Il nome è obbligatorio.')
return
}
setUploading(true);
const fd = new FormData();
fd.append("file", file);
fd.append("name", name.trim());
fd.append("author", author.trim());
setUploading(true)
const fd = new FormData()
fd.append('file', file)
fd.append('name', name.trim())
fd.append('author', author.trim())
try {
const res = await fetch("/api/admin/sounds/upload", { method: "POST", body: fd });
const json = (await res.json()) as { ok?: boolean; error?: string; id?: number };
const res = await fetch('/api/admin/sounds/upload', { method: 'POST', body: fd })
const json = (await res.json()) as { ok?: boolean; error?: string; id?: number }
if (!res.ok || !json.ok) {
toast.error(json.error || "Upload fallito.");
return;
toast.error(json.error || 'Upload fallito.')
return
}
toast.success(`Song disk #${json.id} caricato.`);
reset();
onOpenChange(false);
onUploaded();
toast.success(`Song disk #${json.id} caricato.`)
reset()
onOpenChange(false)
onUploaded()
} catch (err) {
console.error(err);
toast.error("Errore di rete durante l'upload.");
console.error(err)
toast.error("Errore di rete durante l'upload.")
} finally {
setUploading(false);
setUploading(false)
}
}
@@ -389,15 +409,15 @@ function UploadDialog({
<Dialog
open={open}
onOpenChange={(o) => {
if (!uploading) onOpenChange(o);
if (!uploading) onOpenChange(o)
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Upload Song Disk</DialogTitle>
<DialogDescription>
Carica un file MP3. Dimensione massima 10 MB. Il codice sarà generato automaticamente come{" "}
<code>song_&#123;id&#125;</code>.
Carica un file MP3. Dimensione massima 10 MB. Il codice sarà generato automaticamente
come <code>song_&#123;id&#125;</code>.
</DialogDescription>
</DialogHeader>
@@ -429,7 +449,12 @@ function UploadDialog({
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={uploading}>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={uploading}
>
Annulla
</Button>
<Button type="submit" disabled={uploading}>
@@ -444,7 +469,7 @@ function UploadDialog({
</form>
</DialogContent>
</Dialog>
);
)
}
// ── Edit dialog ──────────────────────────────────────────────────────
@@ -454,28 +479,28 @@ function EditDialog({
onOpenChange,
onSaved,
}: {
soundtrack: SoundtrackRow;
onOpenChange: (open: boolean) => void;
onSaved: () => void;
soundtrack: SoundtrackRow
onOpenChange: (open: boolean) => void
onSaved: () => void
}) {
const [name, setName] = useState(soundtrack.name);
const [author, setAuthor] = useState(soundtrack.author);
const [track, setTrack] = useState(soundtrack.track ?? "");
const [length, setLength] = useState(String(soundtrack.length ?? 0));
const { run, isPending } = useServerAction();
const [name, setName] = useState(soundtrack.name)
const [author, setAuthor] = useState(soundtrack.author)
const [track, setTrack] = useState(soundtrack.track ?? '')
const [length, setLength] = useState(String(soundtrack.length ?? 0))
const { run, isPending } = useServerAction()
const isComposition = soundtrack.kind === "composition";
const isComposition = soundtrack.kind === 'composition'
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
e.preventDefault()
if (!name.trim()) {
toast.error("Il nome è obbligatorio.");
return;
toast.error('Il nome è obbligatorio.')
return
}
const lenNum = Number.parseInt(length, 10);
const lenNum = Number.parseInt(length, 10)
if (!Number.isFinite(lenNum) || lenNum < 0) {
toast.error("La durata deve essere un numero ≥ 0.");
return;
toast.error('La durata deve essere un numero ≥ 0.')
return
}
run(
() =>
@@ -483,32 +508,35 @@ function EditDialog({
id: soundtrack.id,
name: name.trim(),
author: author.trim(),
track: isComposition ? track : (soundtrack.track ?? ""),
track: isComposition ? track : (soundtrack.track ?? ''),
length: lenNum,
}),
{
successMessage: "Song disk aggiornato.",
errorMessage: "Impossibile aggiornare il song disk.",
successMessage: 'Song disk aggiornato.',
errorMessage: 'Impossibile aggiornare il song disk.',
onSuccess: onSaved,
},
);
)
}
return (
<Dialog open onOpenChange={(o) => !o && onOpenChange(false)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Modifica {isComposition ? "Trax Composition" : "Song Disk"}</DialogTitle>
<DialogTitle>Modifica {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 di {soundtrack.samples.length} sample. Modifica il campo{' '}
<code>track</code> solo se sai cosa stai facendo.
</>
) : (
<> — il file MP3 non può essere sostituito: elimina e ricarica se devi cambiare audio.</>
<>
{' '}
— il file MP3 non può essere sostituito: elimina e ricarica se devi cambiare audio.
</>
)}
</DialogDescription>
</DialogHeader>
@@ -544,8 +572,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."}
? 'Durata totale della composition. Lascia 0 se sconosciuta.'
: 'Impostato automaticamente dall’upload. Modifica solo se errato.'}
</p>
</div>
@@ -561,15 +589,20 @@ 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: <span className="font-mono">{soundtrack.samples.join(", ") || "—"}</span>
.
Formato: <code>canale:sample_id,durata;sample_id,durata;…</code> — <code>0</code> =
silenzio. Sample IDs rilevati:{' '}
<span className="font-mono">{soundtrack.samples.join(', ') || '—'}</span>.
</p>
</div>
)}
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isPending}>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isPending}
>
Annulla
</Button>
<Button type="submit" disabled={isPending}>
@@ -580,5 +613,5 @@ function EditDialog({
</form>
</DialogContent>
</Dialog>
);
)
}