diff --git a/.gitignore b/.gitignore index bd10a86..dbc8749 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ legacy/Scores _builds/ +_deploy_temp/ missing.xml partitions/.svelte-kit/ partitions/build/ diff --git a/AGENTS.md b/AGENTS.md index 5573e03..b6e2c04 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -109,6 +109,46 @@ npm run preview - Do NOT include `legacy/` or `api/tests.php` in the deploy archive. - `api/.env` (with `JWT_SECRET`) is part of the deployment. - The frontend build must be present in `frontend/`. +- Production: front `https://partitions.c.nadal-fr.com`, API `https://ohmj-api.c.nadal-fr.com`. +- **Cache**: hashed assets under `_app/immutable/` are immutable (1y); the SPA + shell `index.html` MUST be served with `Cache-Control: no-cache` (see + `partitions/nginx.conf`). Otherwise clients keep a stale shell referencing + hashed chunks removed by the new deploy, causing errors on some routes (e.g. + the PDF viewer). The front is served by the `nginx` container (Caddy only + `reverse_proxy`s to it and passes the headers through), so this is configured + in `nginx.conf`, not in the Caddyfile. After a deploy, a hard refresh can be + needed for already-cached clients. + +### Testing the deploy archive (recommended) + +It is worth testing the produced zip, not just the source tree. `deploy.sh` +generates `_builds/deploy_ohmj.zip`; verify it before uploading: + +```bash +# 1. Inspect the archive (no stale/test/residue files) +unzip -l _builds/deploy_ohmj.zip | grep -E 'tests\.php|coverage-|composer\.phar|node_modules|legacy/' +# -> must print nothing +unzip -p _builds/deploy_ohmj.zip api/.env # JWT_SECRET + SCORES_PATH +# 2. Extract and run the artifact end-to-end against the E2E smoke test +rm -rf /tmp/deploy_test && mkdir -p /tmp/deploy_test +unzip -q _builds/deploy_ohmj.zip -d /tmp/deploy_test +# - API: cd /tmp/deploy_test/api +# SCORES_PATH=/legacy/Scores/ php -c php-upload.ini -S localhost:8000 router.php +# - front: serve /tmp/deploy_test/frontend with an SPA fallback (see below) +API_URL=http://localhost:8000 FRONT_URL=http://localhost:4173 npm run test:e2e --prefix partitions +# 3. Security: .env / config/users.json must NOT be served +curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8000/.env # -> 401 +``` + +Notes: +- `deploy.sh` deletes the previous zip before zipping: `zip -r` alone would + **merge** into an existing archive and keep stale files from earlier deploys. +- The built frontend bakes `VITE_API_URL` **and** the CSP `connect-src` + (`svelte.config.js` only allows `localhost:8000` + the prod API). To test a zip + locally, either reuse `localhost:8000` or rebuild the front with a permitted URL + (`VITE_API_URL=http://localhost:8000 npm run build`). +- The E2E smoke test accepts `API_URL` / `FRONT_URL` so it can target the + extracted artifact instead of the dev servers. ## Testing @@ -132,7 +172,7 @@ rm -f /tmp/rate_* 2>/dev/null # clear rate limiting fir php tests.php ``` -- Expected: **53/53 tests passed (100%)**. +- Expected: **183/183 tests passed (100%)**. - Covers: auth (login, bad credentials, missing/invalid token), scores CRUD, create-score-with-pieces, files tree, path traversal, rate limiting, upload validation, INI injection, security headers. @@ -268,6 +308,12 @@ Instrument codes: `dir`, `pic`, `flu`, `cla`, `clb`, `sax`, `sat`, `sab`, `coa`, `cba`, `cor`, `trp`, `trb`, `tub`, `htb`, `bas`, `per`, `crn`, `eup`, `har`, `pia`, plus `sup`/`par` for combined parts. +Admin uploads build the filename from the instrument's **ASCII** name +(`instruments.ts` → `api/lib/ScoreScanner.php`), with `-` between fields +(e.g. `clarinette-sib-1.pdf`). For `sup`/`par`, the **"Parties combinées"** editor +builds the real filename base from one or more instruments (with key/clef/part), +joined by `+` (e.g. `contrebasse-sib-clefa-1+baryton-sib-clefa-1`). See `NAMING.md`. + ## Code Style ### PHP @@ -300,7 +346,7 @@ plus `sup`/`par` for combined parts. - [ ] CORS origins restricted to the real domains (no wildcard fallback). - [ ] HTTPS enforced in production. - [ ] Rate limiting active (clear `/tmp/rate_*` only in tests, never in prod). -- [ ] `php tests.php` passes 53/53. +- [ ] `php tests.php` passes 183/183. - [ ] `npm audit` reviewed. - [ ] No secrets in code or git history. diff --git a/NAMING.md b/NAMING.md index 530a97d..07eab27 100644 --- a/NAMING.md +++ b/NAMING.md @@ -52,6 +52,23 @@ legacy/Scores//score.ini | `glockenspiel-1+xylophone-1.pdf` | percussion multiple | | `percussion-1+gong-1+cymbale-1+triangle-1.pdf` | 3 `+` possibles | +> Ordre réel des champs d'un nom simple : `instrument`, puis `tonalité`, +> `variante`, `clef`, et enfin `partie` (ex. `euphonium-sib-clesol-1.pdf`, +> `cornet-sib-solo-1.pdf`, `baryton-clefa-1.pdf`). + +### Parties combinées (`sup` / `par`) + +Les dossiers `sup/` et `par/` regroupent des partitions **combinées** : le nom du +fichier reprend les **instruments réels** reliés par `+`, pas le code du dossier. +Exemples réels : `331/1/sup/1/contrebasse-1+clarinette-1.pdf`, +`295/1/sup/1/euphonium-sib-clefa-1+baryton-sib-clefa-1.pdf`, +`346/1/par/1/partie-sib-4.pdf`. + +À l'upload (admin), un éditeur **« Parties combinées »** apparaît pour `sup`/`par` : +on ajoute un ou plusieurs instruments, chacun avec **tonalité / clé / partie** ; le +nom est alors construit automatiquement, relié par `+` +(ex. `contrebasse-sib-clefa-1+baryton-sib-clefa-1.pdf`). + ### Tonalités `sib` (Si♭), `mib` (Mi♭), `fa` (Fa), `ut` (Do — **`do` s'écrit `ut` dans les diff --git a/README.md b/README.md index 6a8b98b..7408d34 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Détail manuel : ```bash cd api rm -f /tmp/rate_* 2>/dev/null -php tests.php # attendu : 181/181 +php tests.php # attendu : 183/183 ``` Frontend (serveurs API + dev lancés) : @@ -71,6 +71,10 @@ npm run test:e2e # smoke E2E navigateur (CDP) Le build frontend doit être présent dans `frontend/`. `legacy/` et `api/tests.php` ne sont pas inclus dans l'archive. +> Il est recommandé de **tester l'archive** (`_builds/deploy_ohmj.zip`) avant de +> l'uploader : inspection du contenu + lancement du smoke test E2E sur l'artefact +> extrait. Procédure détaillée dans [`AGENTS.md`](AGENTS.md#testing-the-deploy-archive-recommended). + ## Documentation - [`AGENTS.md`](AGENTS.md) — guide de développement (stack, commandes, conventions) diff --git a/api/index.php b/api/index.php index 71a09d9..0875a7f 100644 --- a/api/index.php +++ b/api/index.php @@ -417,8 +417,9 @@ if (preg_match('#^admin/scores/(\d+)$#', $path, $matches) && $method === 'DELETE $part = $_POST['part'] ?? '1'; $watermark = isset($_POST['watermark']) && $_POST['watermark'] === 'true'; $watermarkPosition = $_POST['watermarkPosition'] ?? 'left'; + $combined = $_POST['combined'] ?? ''; - $result = $scanner->uploadPdf($scoreId, $file, $piece, $instrument, $version, $key, $clef, $variant, $part, $watermark, $watermarkPosition); + $result = $scanner->uploadPdf($scoreId, $file, $piece, $instrument, $version, $key, $clef, $variant, $part, $watermark, $watermarkPosition, $combined); if ($result['success']) { echo json_encode(['success' => true, 'path' => $result['path']]); diff --git a/api/lib/ScoreScanner.php b/api/lib/ScoreScanner.php index cc71820..97d34da 100644 --- a/api/lib/ScoreScanner.php +++ b/api/lib/ScoreScanner.php @@ -594,7 +594,7 @@ class ScoreScanner { rmdir($dir); } - public function uploadPdf(string $scoreId, array $file, string $piece, string $instrument, string $version, string $key = '', string $clef = '', string $variant = '', string $part = '1', bool $watermark = false, string $watermarkPosition = 'left'): array { + public function uploadPdf(string $scoreId, array $file, string $piece, string $instrument, string $version, string $key = '', string $clef = '', string $variant = '', string $part = '1', bool $watermark = false, string $watermarkPosition = 'left', ?string $combined = null): array { // Security: every input used to build a path or a filename is validated if (!$this->isValidSegment($scoreId, '/^\d{1,6}$/')) { return ['success' => false, 'error' => 'Invalid score id']; @@ -680,20 +680,19 @@ class ScoreScanner { $instName = $instrumentNames[$instrument] ?? $instrument; - // Build filename: instrument_variant_key_clef_part.pdf - $filenameParts = [$instName]; - if (!empty($variant)) { - $filenameParts[] = $variant; + if (in_array($instrument, ['sup', 'par'], true) && $combined !== null && trim($combined) !== '') { + // Combined/superposed parts keep their real instrument names (e.g. "contrebasse-1+clarinette-1") + $combined = strtolower(trim($combined)); + if (!$this->isValidSegment($combined, '/^[a-z0-9_+.-]+$/')) { + return ['success' => false, 'error' => 'Invalid combined instruments']; + } + $filename = $combined . '.pdf'; + } else { + // NAMING.md: fields separated by '-', compound names/suffixes by '_' + $nameParts = array_values(array_filter([$instName, $key, $variant, $clef], fn($v) => $v !== '')); + $nameParts[] = $part; + $filename = implode('-', $nameParts) . '.pdf'; } - if (!empty($key)) { - $filenameParts[] = $key; - } - if (!empty($clef)) { - $filenameParts[] = $clef; - } - $filenameParts[] = $part; - - $filename = implode('_', $filenameParts) . '.pdf'; $targetPath = $targetDir . '/' . $filename; diff --git a/api/tests.php b/api/tests.php index 407e85e..0a22075 100644 --- a/api/tests.php +++ b/api/tests.php @@ -330,7 +330,7 @@ class APITest { // Get files to verify it was uploaded (recursive tree) $files = $this->request('GET', "admin/scores/$scoreId/files"); - $found = $this->findTreeFile($files['body']['files'] ?? [], 'direction_1.pdf'); + $found = $this->findTreeFile($files['body']['files'] ?? [], 'direction-1.pdf'); if ($found) { require_once __DIR__ . '/vendor/autoload.php'; $check = new \setasign\Fpdi\Fpdi(); @@ -357,7 +357,7 @@ class APITest { // Clean up all uploaded files (recursive) then the dedicated score $files = $this->request('GET', "admin/scores/$scoreId/files"); - $this->deleteTreeMatching($files['body']['files'] ?? [], $scoreId, 'direction_'); + $this->deleteTreeMatching($files['body']['files'] ?? [], $scoreId, 'direction-'); $this->request('DELETE', "admin/scores/$scoreId"); } @@ -682,6 +682,17 @@ class APITest { $noFile = $this->request('POST', "admin/scores/$scoreId/upload", ['unexpected' => 'body']); $this->test('Upload without file part returns 413', $noFile['code'] === 413); + // Combined/superposed parts are named after the combined instruments + $supUp = $this->multipartRequest("admin/scores/$scoreId/upload", [ + ['name' => 'file', 'file' => $tmpPdf], + ['name' => 'piece', 'data' => '1'], + ['name' => 'instrument', 'data' => 'sup'], + ['name' => 'version', 'data' => '1'], + ['name' => 'combined', 'data' => 'contrebasse-1+clarinette-1'], + ]); + $this->test('Upload sup combined returns 200', $supUp['code'] === 200); + $this->test('Upload sup combined filename', str_contains($supUp['body']['path'] ?? '', 'contrebasse-1+clarinette-1.pdf')); + // Instruments (list + filter) $instr = $this->request('GET', "scores/$scoreId/instruments"); $this->test('Instruments returns 200', $instr['code'] === 200); @@ -693,7 +704,7 @@ class APITest { $this->test('Pieces returns 200', $pieces['code'] === 200); // Download success + failures - $path = "$scoreId/1/dir/1/direction_1.pdf"; + $path = "$scoreId/1/dir/1/direction-1.pdf"; $ok = $this->request('GET', "download/$path"); $this->test('Download with token returns 200', $ok['code'] === 200); $noToken = $this->request('GET', "download/$path", null, false); @@ -742,7 +753,7 @@ class APITest { } // Delete uploaded file, then the score - $del = $this->request('DELETE', "admin/scores/$scoreId/files", null, true, ['path' => '1/dir/1/direction_1.pdf']); + $del = $this->request('DELETE', "admin/scores/$scoreId/files", null, true, ['path' => '1/dir/1/direction-1.pdf']); $this->test('Delete file returns 200', $del['code'] === 200); $delScore = $this->request('DELETE', "admin/scores/$scoreId"); $this->test('Delete score returns 200', $delScore['code'] === 200); @@ -950,9 +961,9 @@ class APITest { @chmod($base . '100/1/flu/1', 0555); $this->test('Unit: upload copy failure', $scanner->uploadPdf('100', $file, '1', 'flu', '1')['success'] === false); @chmod($base . '100/1/flu/1', 0755); - @mkdir($base . '100/1/dir/1/direction_1.pdf', 0777, true); + @mkdir($base . '100/1/dir/1/direction-1.pdf', 0777, true); $this->test('Unit: uploadPdf rename failure', $scanner->uploadPdf('100', $wmFile, '1', 'dir', '1', '', '', '', '1', true, 'left')['success'] === false); - @rmdir($base . '100/1/dir/1/direction_1.pdf'); + @rmdir($base . '100/1/dir/1/direction-1.pdf'); $this->test('Unit: getScoreFiles unknown', $scanner->getScoreFiles('999')['success'] === false); $this->test('Unit: getScoreFiles ok', $scanner->getScoreFiles('100')['success'] === true); @@ -970,7 +981,7 @@ class APITest { @mkdir($base . '100/1/cla/1/subdir', 0777, true); $this->test('Unit: deleteFile directory', $scanner->deleteScoreFile('100', '1/cla/1/subdir')['success'] === false); @chmod($base . '100/1/flu/1', 0555); - $this->test('Unit: deleteFile unlink failure', $scanner->deleteScoreFile('100', '1/flu/1/flute_ut_clesol_1.pdf')['success'] === false); + $this->test('Unit: deleteFile unlink failure', $scanner->deleteScoreFile('100', '1/flu/1/flute-ut-clesol-1.pdf')['success'] === false); @chmod($base . '100/1/flu/1', 0755); // deleteScore + deleteDirectory diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 61899b9..86fcea4 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -47,8 +47,9 @@ cp partitions/nginx.conf $DEPLOY_DIR/frontend/nginx.conf 2>/dev/null || echo "ng # 7. Create zip (without tests.php and legacy) echo "[7/8] Creating zip..." +rm -f ../_builds/deploy_ohmj.zip cd $DEPLOY_DIR -zip -r ../_builds/deploy_ohmj.zip api frontend -x "*.DS_Store" "node_modules/*" ".svelte-kit/*" "api/tests.php" "api/coverage-prepend.php" "api/coverage-report.php" +zip -r ../_builds/deploy_ohmj.zip api frontend -x "*.DS_Store" "node_modules/*" ".svelte-kit/*" "api/tests.php" "api/coverage-prepend.php" "api/coverage-report.php" "api/composer.phar" # 8. Cleanup echo "[8/8] Cleaning up..." diff --git a/partitions/nginx.conf b/partitions/nginx.conf index 0b21f3d..d87145c 100644 --- a/partitions/nginx.conf +++ b/partitions/nginx.conf @@ -10,26 +10,38 @@ server { gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + # Security headers. + # NOTE: nginx does not inherit `add_header` into a location that declares its + # own add_header, so the asset/worker locations below repeat them. + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + # PDF.js worker: serve the tracked .mjs with a JS MIME type location = /pdf.worker.min.mjs { default_type application/javascript; expires 1y; add_header Cache-Control "public, immutable"; + add_header X-Content-Type-Options "nosniff" always; } - # Cache static assets + # Hashed assets are immutable (safe to cache for a long time) location ~* \.(js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; + add_header X-Content-Type-Options "nosniff" always; } - # All routes should serve index.html (SPA) + # SPA shell: never cache it, it references hashed chunks removed on deploy. + # `expires -1` emits `Cache-Control: no-cache` without using add_header, so + # the server-level security headers above are still inherited. + location = /index.html { + expires -1; + } + + # All routes fall back to index.html (SPA) location / { + expires -1; try_files $uri $uri/ /index.html; } - - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; } diff --git a/partitions/src/app.css b/partitions/src/app.css index 2a2c121..589bbec 100644 --- a/partitions/src/app.css +++ b/partitions/src/app.css @@ -2,15 +2,63 @@ @tailwind components; @tailwind utilities; -:root { - --ohmj-primary: #1e3a5f; - --ohmj-secondary: #c9a227; - --ohmj-light: #f5f5f5; - --ohmj-dark: #0a1f33; +@layer base { + html { + -webkit-text-size-adjust: 100%; + } + + body { + @apply bg-ohmj-light text-gray-800 antialiased; + } + + /* Consistent, visible keyboard focus everywhere */ + :focus-visible { + outline: 2px solid theme('colors.ohmj.primary'); + outline-offset: 2px; + } + + @media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + } } -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; - background-color: var(--ohmj-light); - color: #333; +@layer components { + .btn { + @apply inline-flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50; + } + .btn-primary { + @apply bg-ohmj-primary text-white hover:bg-ohmj-secondary; + } + .btn-ghost { + @apply text-gray-600 hover:bg-gray-100 hover:text-gray-800; + } + .btn-danger { + @apply bg-red-600 text-white hover:bg-red-700; + } + + .card { + @apply rounded-xl border border-gray-200 bg-white shadow-sm; + } + + .input { + @apply block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-ohmj-primary focus:outline-none focus:ring-2 focus:ring-ohmj-primary/30; + } + .label { + @apply block text-sm font-medium text-gray-700; + } + + .badge { + @apply inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700; + } + + .alert-error { + @apply rounded-md border border-red-400 bg-red-100 px-4 py-3 text-red-700; + } } diff --git a/partitions/src/lib/api.ts b/partitions/src/lib/api.ts index 9297f57..238df2f 100644 --- a/partitions/src/lib/api.ts +++ b/partitions/src/lib/api.ts @@ -148,7 +148,7 @@ export const apiService = { return response.data; }, - async uploadPdf(scoreId: string, file: File, piece: string, instrument: string, version: string, key?: string, clef?: string, variant?: string, part?: string, watermark?: boolean, watermarkPosition?: 'left' | 'right'): Promise<{ success: boolean; path?: string; error?: string }> { + async uploadPdf(scoreId: string, file: File, piece: string, instrument: string, version: string, key?: string, clef?: string, variant?: string, part?: string, watermark?: boolean, watermarkPosition?: 'left' | 'right', combined?: string): Promise<{ success: boolean; path?: string; error?: string }> { const formData = new FormData(); formData.append('file', file); formData.append('piece', piece); @@ -160,6 +160,7 @@ export const apiService = { if (part) formData.append('part', part); if (watermark) formData.append('watermark', 'true'); if (watermarkPosition) formData.append('watermarkPosition', watermarkPosition); + if (combined) formData.append('combined', combined); const response = await api.post(`/admin/scores/${scoreId}/upload`, formData, { headers: { diff --git a/partitions/src/lib/components/Alert.svelte b/partitions/src/lib/components/Alert.svelte new file mode 100644 index 0000000..5181064 --- /dev/null +++ b/partitions/src/lib/components/Alert.svelte @@ -0,0 +1,7 @@ + + +{#if message} + +{/if} diff --git a/partitions/src/lib/components/BackLink.svelte b/partitions/src/lib/components/BackLink.svelte new file mode 100644 index 0000000..59f8aac --- /dev/null +++ b/partitions/src/lib/components/BackLink.svelte @@ -0,0 +1,52 @@ + + +{#if variant === 'icon'} + + + + + +{:else if variant === 'ghost'} + + + + + {label} + +{:else} + + + + + {label} + +{/if} diff --git a/partitions/src/lib/components/InstrumentIcon.svelte b/partitions/src/lib/components/InstrumentIcon.svelte new file mode 100644 index 0000000..13d1577 --- /dev/null +++ b/partitions/src/lib/components/InstrumentIcon.svelte @@ -0,0 +1,39 @@ + + +{#if icon} + {title} +{:else} + +{/if} diff --git a/partitions/src/lib/components/PdfViewer.svelte b/partitions/src/lib/components/PdfViewer.svelte index 121b3c4..4e0736b 100644 --- a/partitions/src/lib/components/PdfViewer.svelte +++ b/partitions/src/lib/components/PdfViewer.svelte @@ -2,6 +2,7 @@ import { onMount, onDestroy, tick } from 'svelte'; import * as pdfjsLib from 'pdfjs-dist'; import { api } from '$lib/api'; + import Spinner from '$lib/components/Spinner.svelte'; pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs'; @@ -189,38 +190,42 @@ -
+
{#if loading} -
-
-

Chargement du PDF...

+
+ +

Chargement du PDF...

{:else if error} -
-

⚠️

+
+ + +

{error}

{:else} - + {/if}
-
- - +
{#each Array(Math.min(5, totalPages)) as _, i} {@const pageNum = Math.max(1, Math.min(currentPage - 2, totalPages - 4)) + i} {#if pageNum <= totalPages} @@ -228,12 +233,11 @@ {/each}
-
diff --git a/partitions/src/lib/components/Spinner.svelte b/partitions/src/lib/components/Spinner.svelte new file mode 100644 index 0000000..90d2338 --- /dev/null +++ b/partitions/src/lib/components/Spinner.svelte @@ -0,0 +1,9 @@ + + +
diff --git a/partitions/src/lib/instruments.ts b/partitions/src/lib/instruments.ts index 0ee34bd..3f2f851 100644 --- a/partitions/src/lib/instruments.ts +++ b/partitions/src/lib/instruments.ts @@ -3,32 +3,34 @@ export interface InstrumentInfo { name: string; icon: string; defaultKey: string; + /** ASCII basename used to build the PDF filename (see NAMING.md). */ + file: string; } export const INSTRUMENTS: InstrumentInfo[] = [ - { code: 'dir', name: 'Direction', icon: '🎼', defaultKey: '' }, - { code: 'pic', name: 'Piccolo', icon: '/icons/piccolo.png', defaultKey: 'do' }, - { code: 'flu', name: 'Flûte', icon: '/icons/flute.png', defaultKey: 'do' }, - { code: 'cla', name: 'Clarinette', icon: '/icons/clarinette.png', defaultKey: 'sib' }, - { code: 'clb', name: 'Clarinette Basse', icon: '/icons/clarinette_basse.png', defaultKey: 'sib' }, - { code: 'sax', name: 'Sax Alto', icon: '/icons/sax-alto.png', defaultKey: 'mib' }, - { code: 'sat', name: 'Sax Ténor', icon: '/icons/sax-tenor.png', defaultKey: 'sib' }, - { code: 'sab', name: 'Sax Baryton', icon: '/icons/sax-barython.png', defaultKey: 'mib' }, - { code: 'coa', name: 'Cor Anglais', icon: '/icons/cor-anglais.jpg', defaultKey: 'fa' }, - { code: 'cba', name: 'Contrebasse', icon: '/icons/contrebasse.png', defaultKey: 'sib' }, - { code: 'cor', name: 'Cor', icon: '/icons/cor.png', defaultKey: 'fa' }, - { code: 'trp', name: 'Trompette', icon: '/icons/trumpet.png', defaultKey: 'sib' }, - { code: 'crn', name: 'Cornet', icon: '/icons/cornet.png', defaultKey: 'sib' }, - { code: 'trb', name: 'Trombone', icon: '/icons/tronbonne.png', defaultKey: 'do' }, - { code: 'eup', name: 'Euphonium', icon: '/icons/euphonium.png', defaultKey: 'sib' }, - { code: 'tub', name: 'Tuba', icon: '/icons/tuba.png', defaultKey: 'do' }, - { code: 'htb', name: 'Hautbois', icon: '/icons/hautbois.png', defaultKey: 'do' }, - { code: 'bas', name: 'Basson', icon: '/icons/basson.png', defaultKey: 'do' }, - { code: 'per', name: 'Percussions', icon: '/icons/percussion.png', defaultKey: '' }, - { code: 'pia', name: 'Piano', icon: '🎹', defaultKey: '' }, - { code: 'har', name: 'Harpe', icon: '/icons/harpe.png', defaultKey: '' }, - { code: 'sup', name: 'Parties supplémentaires', icon: '📄', defaultKey: '' }, - { code: 'par', name: 'Parties', icon: '📄', defaultKey: '' } + { code: 'dir', name: 'Direction', icon: '', defaultKey: '', file: 'direction' }, + { code: 'pic', name: 'Piccolo', icon: '/icons/piccolo.png', defaultKey: 'do', file: 'piccolo' }, + { code: 'flu', name: 'Flûte', icon: '/icons/flute.png', defaultKey: 'do', file: 'flute' }, + { code: 'cla', name: 'Clarinette', icon: '/icons/clarinette.png', defaultKey: 'sib', file: 'clarinette' }, + { code: 'clb', name: 'Clarinette Basse', icon: '/icons/clarinette_basse.png', defaultKey: 'sib', file: 'clarinette_basse' }, + { code: 'sax', name: 'Sax Alto', icon: '/icons/sax-alto.png', defaultKey: 'mib', file: 'saxophone_alto' }, + { code: 'sat', name: 'Sax Ténor', icon: '/icons/sax-tenor.png', defaultKey: 'sib', file: 'saxophone_tenor' }, + { code: 'sab', name: 'Sax Baryton', icon: '/icons/sax-barython.png', defaultKey: 'mib', file: 'saxophone_baryton' }, + { code: 'coa', name: 'Cor Anglais', icon: '/icons/cor-anglais.jpg', defaultKey: 'fa', file: 'cor_anglais' }, + { code: 'cba', name: 'Contrebasse', icon: '/icons/contrebasse.png', defaultKey: 'sib', file: 'contrebasse' }, + { code: 'cor', name: 'Cor', icon: '/icons/cor.png', defaultKey: 'fa', file: 'cor' }, + { code: 'trp', name: 'Trompette', icon: '/icons/trumpet.png', defaultKey: 'sib', file: 'trompette' }, + { code: 'crn', name: 'Cornet', icon: '/icons/cornet.png', defaultKey: 'sib', file: 'cornet' }, + { code: 'trb', name: 'Trombone', icon: '/icons/tronbonne.png', defaultKey: 'do', file: 'trombone' }, + { code: 'eup', name: 'Euphonium', icon: '/icons/euphonium.png', defaultKey: 'sib', file: 'euphonium' }, + { code: 'tub', name: 'Tuba', icon: '/icons/tuba.png', defaultKey: 'do', file: 'tuba' }, + { code: 'htb', name: 'Hautbois', icon: '/icons/hautbois.png', defaultKey: 'do', file: 'hautbois' }, + { code: 'bas', name: 'Basson', icon: '/icons/basson.png', defaultKey: 'do', file: 'basson' }, + { code: 'per', name: 'Percussions', icon: '/icons/percussion.png', defaultKey: '', file: 'percussion' }, + { code: 'pia', name: 'Piano', icon: '', defaultKey: '', file: 'piano' }, + { code: 'har', name: 'Harpe', icon: '/icons/harpe.png', defaultKey: '', file: 'harpe' }, + { code: 'sup', name: 'Parties supplémentaires', icon: '', defaultKey: '', file: '' }, + { code: 'par', name: 'Parties', icon: '', defaultKey: '', file: '' } ]; export const INSTRUMENT_NAMES: Record = Object.fromEntries( @@ -44,7 +46,7 @@ export function getInstrumentName(code: string): string { } export function getInstrumentIcon(code: string): string { - return INSTRUMENT_ICONS[code] ?? '🎵'; + return INSTRUMENT_ICONS[code] ?? ''; } export function getInstrument(code: string): InstrumentInfo | undefined { @@ -54,3 +56,7 @@ export function getInstrument(code: string): InstrumentInfo | undefined { export function getDefaultKey(code: string): string { return getInstrument(code)?.defaultKey ?? ''; } + +export function getInstrumentFile(code: string): string { + return getInstrument(code)?.file ?? code; +} diff --git a/partitions/src/routes/+layout.svelte b/partitions/src/routes/+layout.svelte index 99df375..ce69dad 100644 --- a/partitions/src/routes/+layout.svelte +++ b/partitions/src/routes/+layout.svelte @@ -25,10 +25,10 @@ } -
+
{#if isAuthenticated && currentPath !== '/'} -
-
+
+
OHMJ @@ -52,12 +49,12 @@
{/if} -
+
{@render children()}
{#if isAuthenticated && currentPath !== '/'} -