[FEAT] Fix some bug and css issues.

This commit is contained in:
NADAL Jean-Baptiste
2026-09-15 11:46:02 +02:00
parent a38fc0bfa9
commit 71d3b71362
26 changed files with 829 additions and 418 deletions
+211 -115
View File
@@ -4,8 +4,10 @@
import { auth } from '$lib/stores/auth';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { INSTRUMENTS, getInstrument } from '$lib/instruments';
import { INSTRUMENTS, getInstrument, getDefaultKey, getInstrumentFile } from '$lib/instruments';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import BackLink from '$lib/components/BackLink.svelte';
import Alert from '$lib/components/Alert.svelte';
let scoreId = $derived(page.params.id || '');
let scoreName = $state('');
@@ -41,6 +43,24 @@
let uploadVariant = $state('');
let uploadPart = $state('1');
let uploadWatermark = $state(false);
// Combined parts (`sup`/`par`): list of instruments with their key/clef/part
type CombinedPart = { instrument: string; key: string; clef: string; part: string };
const KEYS = ['sib', 'mib', 'fa', 'do', 'ut', 'sol', 're', 'mi', 'si'];
const CLEFS = ['clesol', 'clefa', 'cleut'];
let combinedParts = $state<CombinedPart[]>([
{ instrument: '', key: '', clef: '', part: '1' }
]);
let uploadCombined = $derived(
combinedParts
.filter((p) => p.instrument)
.map((p) =>
[getInstrumentFile(p.instrument), p.key, p.clef, p.part]
.filter((v) => v !== '')
.join('-')
)
.join('+')
);
let uploadWatermarkPosition = $state<'left' | 'right'>('left');
// Edit pieces
@@ -128,13 +148,17 @@
function getExpectedFilename(): string {
if (!uploadInstrument) return '';
// sup/par: the file is named after the combined instruments (see NAMING.md)
if (uploadInstrument === 'sup' || uploadInstrument === 'par') {
const combined = uploadCombined.trim().toLowerCase();
return combined ? `${combined}.pdf` : '';
}
const inst = getInstrument(uploadInstrument);
const instName = inst ? inst.name.toLowerCase().replace(/ /g, '_') : uploadInstrument;
const base = inst?.file || uploadInstrument;
const key = uploadKey || inst?.defaultKey || '';
const keyStr = key ? `_${key}` : '';
const clef = uploadClef ? `_${uploadClef}` : '';
const variant = uploadVariant ? `_${uploadVariant}` : '';
return `${instName}${variant}${keyStr}${clef}_${uploadPart}.pdf`;
const parts = [base, key, uploadVariant, uploadClef].filter((v) => v !== '');
parts.push(uploadPart);
return `${parts.join('-')}.pdf`;
}
let expectedFilename = $derived(getExpectedFilename());
@@ -195,7 +219,8 @@
uploadVariant,
uploadPart,
uploadWatermark,
uploadWatermarkPosition
uploadWatermarkPosition,
uploadCombined
);
if (result.success) {
uploadSuccess = 'Fichier uploadé avec succès';
@@ -269,16 +294,14 @@
}
</script>
<div class="max-w-6xl mx-auto p-6">
<div class="container mx-auto px-4 py-8">
<div class="mb-6">
<div class="flex justify-between items-center mb-4">
<span class="text-2xl font-bold text-gray-400">№ {scoreId}</span>
<a href="/admin" class="text-ohmj-primary hover:underline">← Retour à l'admin</a>
</div>
<BackLink href="/admin" label="Retour à l'admin" class="mb-4" />
<!-- Info Card -->
<div class="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden mb-6">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<div class="card mb-6 overflow-hidden">
<div class="flex items-center gap-3 border-b border-gray-200 bg-gray-50 px-6 py-4">
<span class="badge bg-ohmj-secondary/20 font-semibold text-ohmj-primary">№ {scoreId}</span>
<h2 class="text-lg font-semibold text-ohmj-primary">Informations de la partition</h2>
</div>
<div class="p-4">
@@ -358,11 +381,14 @@
</button>
{:else}
{#if scoreRessource}
<button type="button" class="flex-1 text-left text-sm text-blue-600 cursor-pointer hover:text-blue-800 px-3 py-2 rounded-lg hover:bg-gray-50" onclick={() => startEdit('ressource', scoreRessource)} title="Cliquer pour modifier">
🔗 {scoreRessource}
<button type="button" class="flex flex-1 items-center gap-1.5 px-3 py-2 text-left text-sm text-ohmj-primary hover:underline" onclick={() => startEdit('ressource', scoreRessource)} title="Cliquer pour modifier">
<svg class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7.5M10 14 20 4m0 0h-5m5 0v5" />
</svg>
{scoreRessource}
</button>
{:else}
<button type="button" class="flex-1 text-left text-sm text-gray-400 cursor-pointer hover:text-gray-600 px-3 py-2 rounded-lg hover:bg-gray-50" onclick={() => startEdit('ressource', '')} title="Cliquer pour ajouter">
<button type="button" class="flex-1 rounded-lg px-3 py-2 text-left text-sm text-gray-400 hover:bg-gray-50 hover:text-gray-600" onclick={() => startEdit('ressource', '')} title="Cliquer pour ajouter">
Aucune ressource
</button>
{/if}
@@ -375,11 +401,11 @@
</div>
<!-- Parties -->
<div class="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden mb-6">
<div class="card mb-6 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50 flex items-center justify-between">
<h2 class="text-lg font-semibold text-ohmj-primary">Parties ({selectedScorePieceCount})</h2>
{#if !editingPieces}
<button onclick={() => { piecesEdit = [...selectedScorePieces]; editingPieces = true; }} class="text-ohmj-primary hover:text-ohmj-secondary text-sm font-medium">
<button onclick={() => { piecesEdit = [...selectedScorePieces]; editingPieces = true; }} class="btn btn-ghost">
Modifier
</button>
{/if}
@@ -393,7 +419,7 @@
<input
type="text"
bind:value={piece.name}
class="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm"
class="input flex-1"
placeholder="Nom de la partie"
/>
<button onclick={() => piecesEdit = piecesEdit.filter((_, idx) => idx !== i)} class="text-red-500 hover:text-red-700 p-2" title="Supprimer">
@@ -401,12 +427,12 @@
</button>
</div>
{/each}
<button onclick={() => piecesEdit = [...piecesEdit, {id: String(piecesEdit.length + 1), name: ''}]} class="text-ohmj-primary hover:text-ohmj-secondary text-sm font-medium">
<button onclick={() => piecesEdit = [...piecesEdit, {id: String(piecesEdit.length + 1), name: ''}]} class="btn btn-ghost">
+ Ajouter une partie
</button>
<div class="flex gap-2 pt-3 border-t">
<button onclick={() => { piecesEdit = [...selectedScorePieces]; editingPieces = false; }} class="px-4 py-2 text-gray-600 hover:text-gray-800 text-sm">Annuler</button>
<button onclick={savePieces} disabled={savingPieces} class="px-4 py-2 bg-ohmj-primary text-white rounded hover:bg-ohmj-secondary text-sm disabled:opacity-50">
<button onclick={() => { piecesEdit = [...selectedScorePieces]; editingPieces = false; }} class="btn btn-ghost">Annuler</button>
<button onclick={savePieces} disabled={savingPieces} class="btn btn-primary">
{savingPieces ? 'Enregistrement...' : 'Enregistrer'}
</button>
</div>
@@ -423,18 +449,14 @@
</div>
{#if error}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
{/if}
<Alert message={error} class="mb-4" />
{/if}
{#if !isAdmin}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
Accès refusé. Vous devez être administrateur.
</div>
<Alert message="Accès refusé. Vous devez être administrateur." />
{:else}
<!-- Upload PDF -->
<div class="mb-8 p-0 bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
<div class="card mb-8 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h2 class="text-lg font-semibold text-ohmj-primary">Ajouter une partition</h2>
</div>
@@ -442,12 +464,12 @@
<form onsubmit={(e) => { e.preventDefault(); uploadPdf(); }} class="space-y-4">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label for="up-instrument" class="block text-sm font-medium text-gray-700">Instrument</label>
<label for="up-instrument" class="label">Instrument</label>
<select
id="up-instrument"
bind:value={uploadInstrument}
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white"
class="input mt-1"
>
<option value="">Sélectionner...</option>
{#each INSTRUMENTS as inst}
@@ -457,26 +479,90 @@
</div>
</div>
{#if uploadInstrument === 'sup' || uploadInstrument === 'par'}
<div>
<span class="label">Parties combinées</span>
<div class="mt-1 space-y-2">
{#each combinedParts as p, i}
<div class="flex flex-wrap items-center gap-2">
<select
bind:value={p.instrument}
onchange={() => (p.key = getDefaultKey(p.instrument))}
class="input w-44"
aria-label="Instrument {i + 1}"
>
<option value="">Instrument…</option>
{#each INSTRUMENTS.filter((x) => x.file) as inst}
<option value={inst.code}>{inst.name}</option>
{/each}
</select>
<select bind:value={p.key} class="input w-28" aria-label="Tonalité {i + 1}">
<option value="">-</option>
{#each KEYS as k}<option value={k}>{k}</option>{/each}
</select>
<select bind:value={p.clef} class="input w-28" aria-label="Clé {i + 1}">
<option value="">-</option>
{#each CLEFS as c}<option value={c}>{c}</option>{/each}
</select>
<input
bind:value={p.part}
class="input w-20"
placeholder="Partie"
aria-label="Partie {i + 1}"
/>
{#if combinedParts.length > 1}
<button
type="button"
onclick={() => (combinedParts = combinedParts.filter((_, k) => k !== i))}
class="rounded-full p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700"
title="Retirer"
aria-label="Retirer l'instrument {i + 1}"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
{/if}
</div>
{/each}
</div>
<button
type="button"
onclick={() => (combinedParts = [...combinedParts, { instrument: '', key: '', clef: '', part: '1' }])}
class="btn btn-ghost mt-2 px-2 py-1"
>
+ Ajouter un instrument
</button>
<p class="mt-1 text-xs text-gray-400">
Chaque instrument avec sa tonalité/clé/partie, reliés par « + » dans le nom
(ex. <code>contrebasse-sib-clefa-1+baryton-sib-clefa-1</code>).
</p>
</div>
{/if}
<div>
<button
type="button"
onclick={() => { showAdvanced = !showAdvanced; if (showAdvanced && !uploadKey) { uploadKey = INSTRUMENTS.find(i => i.code === uploadInstrument)?.defaultKey || ''; } }}
class="text-sm text-gray-600 hover:text-gray-800"
class="btn btn-ghost"
>
{showAdvanced ? '▼ Masquer' : '▶ Options avancées'}
<svg class="h-4 w-4 transition-transform {showAdvanced ? 'rotate-90' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
{showAdvanced ? 'Masquer les options' : 'Options avancées'}
</button>
</div>
{#if showAdvanced}
<div class="grid grid-cols-2 md:grid-cols-7 gap-4 p-3 bg-gray-100 rounded">
<div class="grid grid-cols-2 gap-4 rounded-md border border-gray-200 bg-gray-50 p-3 md:grid-cols-7">
{#if selectedScorePieceCount > 1}
<div>
<label for="up-piece" class="block text-sm font-medium text-gray-700">Pièce</label>
<label for="up-piece" class="label">Pièce</label>
<select
id="up-piece"
bind:value={uploadPiece}
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white"
class="input mt-1"
>
{#each selectedScorePieces as piece}
<option value={piece.id}>{piece.name}</option>
@@ -485,20 +571,20 @@
</div>
{/if}
<div>
<label for="up-version" class="block text-sm font-medium text-gray-700">Version</label>
<label for="up-version" class="label">Version</label>
<input
id="up-version"
type="number"
min="1"
bind:value={uploadVersion}
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
class="input mt-1"
placeholder="1"
/>
</div>
<div>
<label for="up-key" class="block text-sm font-medium text-gray-700">Tonalité</label>
<select id="up-key" bind:value={uploadKey} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<label for="up-key" class="label">Tonalité</label>
<select id="up-key" bind:value={uploadKey} class="input mt-1">
<option value="">-</option>
<option value="sib">Si♭ (B♭)</option>
<option value="mib">Mi♭ (E♭)</option>
@@ -508,8 +594,8 @@
</select>
</div>
<div>
<label for="up-clef" class="block text-sm font-medium text-gray-700">Clé</label>
<select id="up-clef" bind:value={uploadClef} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<label for="up-clef" class="label">Clé</label>
<select id="up-clef" bind:value={uploadClef} class="input mt-1">
<option value="">-</option>
<option value="clesol">Sol</option>
<option value="clefa">Fa</option>
@@ -517,8 +603,8 @@
</select>
</div>
<div>
<label for="up-part" class="block text-sm font-medium text-gray-700">Partie</label>
<select id="up-part" bind:value={uploadPart} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<label for="up-part" class="label">Partie</label>
<select id="up-part" bind:value={uploadPart} class="input mt-1">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
@@ -526,19 +612,15 @@
</select>
</div>
<div>
<label for="up-variant" class="block text-sm font-medium text-gray-700">Variante</label>
<select id="up-variant" bind:value={uploadVariant} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<label for="up-variant" class="label">Variante</label>
<select id="up-variant" bind:value={uploadVariant} class="input mt-1">
<option value="">-</option>
<option value="solo">Solo</option>
<option value="default">Default</option>
</select>
</div>
<div class="flex items-end">
<button
type="button"
onclick={() => showAdvanced = false}
class="text-sm text-gray-500 hover:text-gray-700"
>
<button type="button" onclick={() => showAdvanced = false} class="btn btn-ghost">
Masquer
</button>
</div>
@@ -546,7 +628,7 @@
{/if}
<div>
<label for="file-input" class="block text-sm font-medium text-gray-700">Fichier PDF</label>
<label for="file-input" class="label">Fichier PDF</label>
<div class="flex items-center gap-3 mt-1">
<label class="relative inline-flex items-center cursor-pointer whitespace-nowrap">
<input type="checkbox" bind:checked={uploadWatermark} class="sr-only peer" />
@@ -556,7 +638,7 @@
{#if uploadWatermark}
<select
bind:value={uploadWatermarkPosition}
class="text-sm border border-gray-300 rounded px-2 py-1"
class="input w-auto"
>
<option value="left">Gauche</option>
<option value="right">Droite</option>
@@ -576,15 +658,17 @@
<p class="text-sm text-gray-500">Nom attendu: <code class="bg-gray-100 px-1 rounded">{expectedFilename}</code></p>
{/if}
{#if uploadError}
<p class="text-red-600 text-sm">{uploadError}</p>
<Alert message={uploadError} />
{/if}
{#if uploadSuccess}
<p class="text-green-600 text-sm">{uploadSuccess}</p>
<p class="rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
{uploadSuccess}
</p>
{/if}
<button
type="submit"
disabled={uploading}
class="bg-ohmj-primary text-white px-4 py-2 rounded hover:bg-ohmj-secondary disabled:opacity-50"
class="btn btn-primary"
>
{uploading ? 'Upload...' : 'Uploader'}
</button>
@@ -593,69 +677,81 @@
</div>
<!-- File Tree -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="p-4 border-b bg-gray-50">
<h2 class="text-lg font-semibold text-ohmj-primary">Fichiers</h2>
</div>
<div class="p-4">
{#if files.length === 0}
<p class="text-gray-500 text-center py-8">Aucun fichier uploadé</p>
{:else}
<div class="font-mono text-sm">
{#each files as piece}
<div class="mb-2">
<span class="text-gray-700 font-semibold">📁 {piece.name}/</span>
{#if piece.children}
<div class="card overflow-hidden">
<div class="border-b bg-gray-50 p-4">
<h2 class="text-lg font-semibold text-ohmj-primary">Fichiers</h2>
</div>
<div class="p-4">
{#if files.length === 0}
<p class="py-8 text-center text-gray-500">Aucun fichier uploadé</p>
{:else}
<div class="space-y-3 text-sm">
{#each files as piece}
<div>
<div class="flex items-center gap-1.5 font-medium text-gray-700">
<svg class="h-4 w-4 text-ohmj-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</svg>
{piece.name}/
</div>
{#if piece.children}
<div class="ml-2 mt-1.5 space-y-2 border-l border-gray-200 pl-3">
{#each piece.children as instrument}
<div class="ml-4">
<span class="text-gray-600">├─ 📁 {instrument.name}/</span>
<div>
<div class="flex items-center gap-1.5 text-gray-600">
<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</svg>
{instrument.name}/
</div>
{#if instrument.children}
{#each instrument.children as version}
<div class="ml-4">
<span class="text-gray-600">├─ 📁 {version.name}/</span>
{#if version.children}
{#each version.children as file, fileIndex}
{@const isLast = fileIndex === version.children.length - 1}
<div class="ml-4">
{#if isLast}
<span class="text-gray-500">└─ 📄 {file.name}</span>
<button
onclick={() => deleteFilePath = file.path}
class="ml-2 text-red-500 hover:text-red-700 p-1 rounded-full hover:bg-red-50"
title="Supprimer"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.997-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
{:else}
<span class="text-gray-500">├─ 📄 {file.name}</span>
<button
onclick={() => deleteFilePath = file.path}
class="ml-2 text-red-500 hover:text-red-700 p-1 rounded-full hover:bg-red-50"
title="Supprimer"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.997-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
<div class="ml-2 mt-1.5 space-y-2 border-l border-gray-200 pl-3">
{#each instrument.children as version}
<div>
<div class="flex items-center gap-1.5 text-gray-500">
<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</svg>
{version.name}/
</div>
{#if version.children}
<ul class="ml-2 mt-1 space-y-0.5 border-l border-gray-200 pl-3">
{#each version.children as file}
<li class="flex items-center gap-1.5">
<svg class="h-4 w-4 shrink-0 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M14 2v6h6" />
</svg>
<span class="truncate font-mono text-gray-600">{file.name}</span>
<button
type="button"
onclick={() => (deleteFilePath = file.path)}
class="ml-1 rounded-full p-1 text-red-500 hover:bg-red-50 hover:text-red-700"
title="Supprimer"
aria-label="Supprimer"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.997-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</li>
{/each}
</ul>
{/if}
</div>
{/each}
{/if}
</div>
{/each}
</div>
{/each}
</div>
{/if}
</div>
{/each}
{/if}
</div>
{/each}
</div>
{/if}
</div>
</div>
{/if}
</div>
{/each}
</div>
{/if}
</div>
</div>
{/if}
<!-- Delete Confirmation Modal -->