[FEAT] We now have a functional APP

This commit is contained in:
NADAL Jean-Baptiste
2026-02-18 12:52:15 +01:00
parent bc6e603af4
commit 1fb0072fc7
30 changed files with 174 additions and 77 deletions

View File

@@ -86,8 +86,11 @@ export const apiService = {
return response.data.score;
},
async getInstruments(scoreId: string): Promise<Instrument[]> {
const response = await api.get(`/scores/${scoreId}/instruments`);
async getInstruments(scoreId: string, pieceId?: number): Promise<Instrument[]> {
const url = pieceId
? `/scores/${scoreId}/instruments?piece=${pieceId}`
: `/scores/${scoreId}/instruments`;
const response = await api.get(url);
return response.data.instruments;
},

View File

@@ -28,6 +28,7 @@
let loading = $state(true);
let error = $state('');
let rendering = $state(false);
let isFullscreen = $state(false);
async function loadPdf() {
loading = true;
@@ -102,8 +103,14 @@
renderPage(currentPage);
}
function toggleFullscreen() {
isFullscreen = !isFullscreen;
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
if (e.key === 'Escape' && isFullscreen) {
isFullscreen = false;
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
prevPage();
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
nextPage();
@@ -129,8 +136,8 @@
});
</script>
<div class="flex flex-col h-full">
<div class="bg-ohmj-dark text-white px-4 py-2 flex items-center justify-between">
<div class="flex flex-col h-full" class:fixed={isFullscreen} class:inset-0={isFullscreen} class:z-50={isFullscreen} class:bg-gray-900={isFullscreen}>
<div class="bg-ohmj-dark text-white px-4 py-2 flex items-center justify-between border-b border-gray-700">
<div class="flex items-center gap-2">
<span class="font-semibold">{title}</span>
{#if totalPages > 0}
@@ -143,7 +150,9 @@
class="px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded text-sm"
title="Zoom -"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
</svg>
</button>
<span class="text-sm w-16 text-center">{Math.round(scale * 100)}%</span>
<button
@@ -151,7 +160,24 @@
class="px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded text-sm"
title="Zoom +"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</button>
<button
onclick={toggleFullscreen}
class="px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded text-sm ml-2"
title={isFullscreen ? 'Quitter plein écran' : 'Plein écran'}
>
{#if isFullscreen}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
</svg>
{/if}
</button>
</div>
</div>
@@ -172,7 +198,7 @@
{/if}
</div>
<div class="bg-ohmj-dark text-white px-4 py-3 flex items-center justify-center gap-4">
<div class="bg-ohmj-dark text-white px-4 py-3 flex items-center justify-center gap-4 border-t border-gray-700">
<button
onclick={prevPage}
disabled={currentPage <= 1}

View File

@@ -48,9 +48,6 @@
</span>
</a>
<nav class="flex items-center gap-4">
<a href="/scores" class="hover:text-ohmj-secondary transition-colors">
Mes Partitions
</a>
<button
onclick={logout}
class="bg-red-600 hover:bg-red-700 px-3 py-1 rounded text-sm transition-colors"

View File

@@ -31,22 +31,23 @@
$: sortedScores = [...scores].sort((a, b) => {
let aVal = a[sortBy] || '';
let bVal = b[sortBy] || '';
let cmp: number;
if (sortBy === 'id') {
aVal = a.id;
bVal = b.id;
cmp = parseInt(a.id) - parseInt(b.id);
} else {
cmp = aVal.localeCompare(bVal);
}
const cmp = aVal.localeCompare(bVal);
return sortOrder === 'asc' ? cmp : -cmp;
});
</script>
<svelte:head>
<title>Mes Partitions - OHMJ</title>
<title>Répertoire - OHMJ</title>
</svelte:head>
<div class="container mx-auto px-4 py-8">
<div class="mb-6">
<h1 class="text-3xl font-bold text-ohmj-primary">Mes Partitions</h1>
<h1 class="text-3xl font-bold text-ohmj-primary">Répertoire</h1>
<p class="text-gray-600 mt-2">{scores.length} partitions disponibles</p>
</div>
@@ -64,7 +65,7 @@
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 w-20"
onclick={() => sortScores('id')}
>
{sortBy === 'id' ? (sortOrder === 'asc' ? '↑' : '↓') : ''}
@@ -81,31 +82,20 @@
>
Compositeur {sortBy === 'compositor' ? (sortOrder === 'asc' ? '↑' : '↓') : ''}
</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Action
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each sortedScores as score}
<tr class="hover:bg-gray-50 transition-colors">
<tr class="hover:bg-ohmj-light transition-colors cursor-pointer" onclick={() => window.location.href = '/scores/' + score.id}>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{score.id}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-ohmj-primary hover:text-ohmj-secondary">
{score.name}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{score.compositor || '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
<a
href="/scores/{score.id}"
class="inline-flex items-center px-3 py-1 border border-ohmj-primary text-sm font-medium rounded text-ohmj-primary hover:bg-ohmj-primary hover:text-white transition-colors"
>
Voir →
</a>
</td>
</tr>
{/each}
</tbody>

View File

@@ -5,10 +5,10 @@
import { goto } from '$app/navigation';
const INSTRUMENT_ICONS: Record<string, string> = {
dir: '🎼', pic: '🎺', flu: '🎵', cla: '🎵', clb: '🎵',
sax: '🎷', sab: '🎷', sat: '🎷', coa: '🎵', cba: '🎸',
cor: '🥇', trp: '🎺', trb: '🎺', tub: '🎺', htb: '🎵',
bas: '🎻', per: '🥁', crn: '🎺', eup: '🎺', har: '🎵',
dir: '🎼', pic: '/icons/piccolo.png', flu: '/icons/flute.png', cla: '/icons/clarinette.png', clb: '/icons/clarinette_basse.png',
sax: '/icons/sax-alto.png', sab: '/icons/sax-barython.png', sat: '/icons/sax-tenor.png', coa: '/icons/cor-anglais.jpg', cba: '/icons/contrebasse.png',
cor: '/icons/cor.png', trp: '/icons/trumpet.png', trb: '/icons/tronbonne.png', tub: '/icons/tuba.png', htb: '/icons/hautbois.png',
bas: '/icons/basson.png', per: '/icons/percussion.png', crn: '/icons/cornet.png', eup: '/icons/euphonium.png', har: '/icons/harpe.png',
pia: '🎹', sup: '📄', par: '📄'
};
@@ -39,6 +39,11 @@
page.subscribe(($page) => {
scoreId = $page.params.id || '';
// Read piece from URL param or query string
const pieceParam = $page.params.piece || $page.url.searchParams.get('piece');
if (pieceParam) {
selectedPiece = parseInt(pieceParam) || 1;
}
});
onMount(async () => {
@@ -46,15 +51,13 @@
score = await apiService.getScore(scoreId);
pieces = await apiService.getPieces(scoreId);
if (pieces.length > 1) {
// Multi-pieces score - use instruments from API
instruments = score?.instruments || [];
loading = false;
} else {
// Single piece - load instruments
selectedPiece = 1;
instruments = score?.instruments || [];
// Set default piece to 1 if not in URL
if (pieces.length > 0 && !pieces.find(p => p.id === selectedPiece)) {
selectedPiece = pieces[0].id;
}
// Load instruments for the selected piece
await loadInstrumentsForPiece(selectedPiece);
} catch (err) {
error = 'Erreur lors du chargement';
console.error(err);
@@ -63,8 +66,21 @@
}
});
async function loadInstrumentsForPiece(pieceId: number) {
loading = true;
try {
instruments = await apiService.getInstruments(scoreId, pieceId);
} catch (err) {
console.error('Error loading instruments:', err);
error = 'Erreur lors du chargement des instruments';
} finally {
loading = false;
}
}
async function selectPiece(pieceId: number) {
goto(`/scores/${scoreId}/${pieceId}`);
selectedPiece = pieceId;
await loadInstrumentsForPiece(pieceId);
}
</script>
@@ -130,14 +146,21 @@
{@const code = instrument.id || instrument.code || ''}
{@const title = instrument.title || getInstrumentName(code)}
{@const fileCount = instrument.parts?.[0]?.files?.length || 0}
{@const icon = getInstrumentIcon(code)}
<a
href="/scores/{scoreId}/{selectedPiece}/{code}"
class="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow border border-gray-200 hover:border-ohmj-primary"
>
<div class="text-center">
<div class="text-4xl mb-2">{getInstrumentIcon(code)}</div>
<div class="text-4xl mb-2">
{#if icon.startsWith('/')}
<img src={icon} alt={title} class="w-12 h-12 mx-auto object-contain" />
{:else}
{icon}
{/if}
</div>
<h3 class="font-semibold text-gray-800">{title}</h3>
<p class="text-sm text-gray-500 mt-1">{fileCount} fichier{fileCount !== 1 ? 's' : ''}</p>
<p class="text-sm text-gray-500 mt-1">{fileCount} partie{fileCount !== 1 ? 's' : ''}</p>
</div>
</a>
{/each}

View File

@@ -4,10 +4,10 @@
import { apiService, type Score, type Instrument } from '$lib/api';
const INSTRUMENT_ICONS: Record<string, string> = {
dir: '🎼', pic: '🎺', flu: '🎵', cla: '🎵', clb: '🎵',
sax: '🎷', sab: '🎷', sat: '🎷', coa: '🎵', cba: '🎸',
cor: '🥇', trp: '🎺', trb: '🎺', tub: '🎺', htb: '🎵',
bas: '🎻', per: '🥁', crn: '🎺', eup: '🎺', har: '🎵',
dir: '🎼', pic: '/icons/piccolo.png', flu: '/icons/flute.png', cla: '/icons/clarinette.png', clb: '/icons/clarinette_basse.png',
sax: '/icons/sax-alto.png', sab: '/icons/sax-barython.png', sat: '/icons/sax-tenor.png', coa: '/icons/cor-anglais.jpg', cba: '/icons/contrebasse.png',
cor: '/icons/cor.png', trp: '/icons/trumpet.png', trb: '/icons/tronbonne.png', tub: '/icons/tuba.png', htb: '/icons/hautbois.png',
bas: '/icons/basson.png', per: '/icons/percussion.png', crn: '/icons/cornet.png', eup: '/icons/euphonium.png', har: '/icons/harpe.png',
pia: '🎹', sup: '📄', par: '📄'
};
@@ -63,12 +63,19 @@
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{#each instruments as instrument}
{@const fileCount = instrument.parts?.[0]?.files?.length || 0}
{@const icon = getInstrumentIcon(instrument.id)}
<a
href="/scores/{scoreId}/{pieceId}/{instrument.id}"
class="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow border border-gray-200 hover:border-ohmj-primary"
>
<div class="text-center">
<div class="text-4xl mb-2">{getInstrumentIcon(instrument.id)}</div>
<div class="text-4xl mb-2">
{#if icon.startsWith('/')}
<img src={icon} alt={instrument.title} class="w-12 h-12 mx-auto object-contain" />
{:else}
{icon}
{/if}
</div>
<h3 class="font-semibold text-gray-800">{instrument.title}</h3>
<p class="text-sm text-gray-500 mt-1">{fileCount} fichier{fileCount !== 1 ? 's' : ''}</p>
</div>

View File

@@ -40,6 +40,8 @@
let error = $state('');
let selectedFile = $state('');
let showSidebar = $state(true);
let pieceName = $state('');
let pieceCount = $state(0);
let pdfViewerUrl = $derived(selectedFile ? apiService.getDownloadUrl(selectedFile) : '');
let pdfFileName = $derived(selectedFile ? selectedFile.split('/').pop() || 'Partition' : 'Partition');
@@ -52,6 +54,14 @@
onMount(async () => {
try {
// Get pieces info
const pieces = await apiService.getPieces(scoreId);
pieceCount = pieces.length;
const piece = pieces.find(p => p.id.toString() === pieceId);
if (piece) {
pieceName = piece.name;
}
const score = await apiService.getScore(scoreId);
const instrument = (score.instruments || []).find(
i => i.id === instrumentCode && i.piece === pieceId
@@ -107,12 +117,12 @@
<div class="h-[calc(100vh-64px)] flex flex-col">
<div class="container mx-auto px-4 py-4">
<a href="/scores/{scoreId}/{pieceId}" class="inline-flex items-center text-ohmj-primary hover:underline mb-4">
<a href="/scores/{scoreId}?piece={pieceId}" class="inline-flex items-center text-ohmj-primary hover:underline mb-4">
← Retour aux instruments
</a>
<h1 class="text-2xl font-bold text-ohmj-primary">
{instrumentTitle || getInstrumentName(instrumentCode)} - Partition {scoreId} - Pièce {pieceId}
Partition {scoreId} - {instrumentTitle || getInstrumentName(instrumentCode)}{pieceCount > 1 ? (pieceName ? ' - ' + pieceName : ' - Pièce ' + pieceId) : ''}
</h1>
</div>
@@ -144,22 +154,26 @@
{#each parts as part}
<div class="mb-4">
<h3 class="font-medium text-gray-800 mb-2">Version {part.id}</h3>
{#if parts.length > 1}
<h3 class="font-medium text-gray-800 mb-2">Version {part.id}</h3>
{/if}
<ul class="space-y-1">
{#each part.files as file}
<li class="flex items-center gap-2">
<li class="flex items-stretch gap-1 rounded overflow-hidden border border-gray-200 hover:border-ohmj-primary transition-colors">
<button
onclick={() => openViewer(file.path)}
class="flex-1 text-left px-3 py-2 rounded hover:bg-ohmj-light border border-transparent hover:border-ohmj-primary transition-colors text-sm {selectedFile === file.path ? 'bg-ohmj-primary text-white' : 'text-ohmj-primary'}"
class="flex-1 text-left px-3 py-2 bg-white hover:bg-ohmj-light text-sm text-ohmj-primary font-medium"
>
📖 {file.name} {getFileInfo(file)}
</button>
<button
onclick={() => downloadFile(file.path)}
class="px-3 py-2 text-gray-500 hover:text-ohmj-secondary transition-colors text-sm"
class="px-3 py-2 bg-gray-50 hover:bg-gray-100 text-gray-600 hover:text-ohmj-primary transition-colors text-sm"
title="Télécharger"
>
⬇️
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>
</li>
{/each}

View File

@@ -1,25 +1,25 @@
<script lang="ts">
const INSTRUMENT_ICONS: Record<string, string> = {
dir: '🎼',
pic: '🎵',
flu: '🎵',
cla: '🎵',
clb: '🎵',
sax: '🎷',
sab: '🎷',
sat: '🎷',
coa: '🎵',
cba: '🎸',
cor: '🥇',
trp: '🎺',
trb: '🎺',
tub: '🎺',
htb: '🎵',
bas: '🎻',
per: '🥁',
crn: '🎺',
eup: '🎺',
har: '🎵',
pic: '/icons/piccolo.png',
flu: '/icons/flute.png',
cla: '/icons/clarinette.png',
clb: '/icons/clarinette_basse.png',
sax: '/icons/sax-alto.png',
sab: '/icons/sax-barython.png',
sat: '/icons/sax-tenor.png',
coa: '/icons/cor-anglais.jpg',
cba: '/icons/contrebasse.png',
cor: '/icons/cor.png',
trp: '/icons/trumpet.png',
trb: '/icons/tronbonne.png',
tub: '/icons/tuba.png',
htb: '/icons/hautbois.png',
bas: '/icons/basson.png',
per: '/icons/percussion.png',
crn: '/icons/cornet.png',
eup: '/icons/euphonium.png',
har: '/icons/harpe.png',
pia: '🎹',
sup: '📄',
par: '📄'
@@ -68,7 +68,13 @@
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{#each instruments as inst}
<div class="bg-white p-6 rounded-lg shadow border border-gray-200 text-center">
<div class="text-5xl mb-2">{inst.icon}</div>
<div class="text-5xl mb-2">
{#if inst.icon.startsWith('/')}
<img src={inst.icon} alt={inst.name} class="w-12 h-12 mx-auto object-contain" />
{:else}
{inst.icon}
{/if}
</div>
<p class="font-semibold">{inst.name}</p>
<p class="text-sm text-gray-400">{inst.code}</p>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB