[FEAT] First functional version.
This commit is contained in:
3035
partitions/package-lock.json
generated
Normal file
3035
partitions/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
partitions/package.json
Normal file
26
partitions/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "partitions",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.32",
|
||||
"svelte": "^5.0.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"lucide-svelte": "^0.574.0",
|
||||
"pdfjs-dist": "^4.0.379"
|
||||
}
|
||||
}
|
||||
6
partitions/postcss.config.js
Normal file
6
partitions/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
16
partitions/src/app.css
Normal file
16
partitions/src/app.css
Normal file
@@ -0,0 +1,16 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--ohmj-primary: #1e3a5f;
|
||||
--ohmj-secondary: #c9a227;
|
||||
--ohmj-light: #f5f5f5;
|
||||
--ohmj-dark: #0a1f33;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background-color: var(--ohmj-light);
|
||||
color: #333;
|
||||
}
|
||||
14
partitions/src/app.d.ts
vendored
Normal file
14
partitions/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
user?: {
|
||||
username: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
13
partitions/src/app.html
Normal file
13
partitions/src/app.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OHMJ - Partitions</title>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
113
partitions/src/lib/api.ts
Normal file
113
partitions/src/lib/api.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import axios, { type AxiosError } from 'axios';
|
||||
import { auth } from '$lib/stores/auth';
|
||||
import { browser } from '$app/environment';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const API_BASE_URL = browser ? 'http://localhost:8000' : 'http://localhost:8000';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
export { api };
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const authState = get(auth);
|
||||
if (authState.token) {
|
||||
config.headers.Authorization = `Bearer ${authState.token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
auth.logout();
|
||||
if (browser) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export interface Score {
|
||||
id: string;
|
||||
name: string;
|
||||
compositor: string;
|
||||
ressource?: string | null;
|
||||
instruments?: Instrument[];
|
||||
}
|
||||
|
||||
export interface Piece {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Instrument {
|
||||
id: string;
|
||||
title: string;
|
||||
piece: string;
|
||||
parts: Part[];
|
||||
}
|
||||
|
||||
export interface Part {
|
||||
id: string;
|
||||
files: PdfFile[];
|
||||
}
|
||||
|
||||
export interface PdfFile {
|
||||
name: string;
|
||||
filename: string;
|
||||
path: string;
|
||||
part: string | null;
|
||||
key: string | null;
|
||||
clef: string | null;
|
||||
variant: string | null;
|
||||
}
|
||||
|
||||
export const apiService = {
|
||||
async login(username: string, password: string): Promise<{ token: string; user: { username: string; role: string } }> {
|
||||
const response = await api.post('/login', { username, password });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getScores(): Promise<Score[]> {
|
||||
const response = await api.get('/scores');
|
||||
return response.data.scores;
|
||||
},
|
||||
|
||||
async getScore(id: string): Promise<Score> {
|
||||
const response = await api.get(`/scores/${id}`);
|
||||
return response.data.score;
|
||||
},
|
||||
|
||||
async getInstruments(scoreId: string): Promise<Instrument[]> {
|
||||
const response = await api.get(`/scores/${scoreId}/instruments`);
|
||||
return response.data.instruments;
|
||||
},
|
||||
|
||||
async getPieces(scoreId: string): Promise<Piece[]> {
|
||||
const response = await api.get(`/pieces/${scoreId}`);
|
||||
return response.data.pieces;
|
||||
},
|
||||
|
||||
async downloadPdf(path: string): Promise<Blob> {
|
||||
const response = await api.get(`/download/${path}`, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getDownloadUrl(path: string): string {
|
||||
let token = '';
|
||||
auth.subscribe((state) => {
|
||||
token = state.token || '';
|
||||
})();
|
||||
return `${API_BASE_URL}/download/${path}?token=${token}`;
|
||||
}
|
||||
};
|
||||
206
partitions/src/lib/components/PdfViewer.svelte
Normal file
206
partitions/src/lib/components/PdfViewer.svelte
Normal file
@@ -0,0 +1,206 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { api } from '$lib/api';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
|
||||
|
||||
interface Props {
|
||||
pdfUrl: string;
|
||||
title?: string;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
let { pdfUrl, title = 'Partition', key = '' }: Props = $props();
|
||||
|
||||
// Force re-render when key changes
|
||||
$effect(() => {
|
||||
if (key) {
|
||||
loadPdf();
|
||||
}
|
||||
});
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
let pdfDoc: pdfjsLib.PDFDocumentProxy | null = null;
|
||||
let currentPage = $state(1);
|
||||
let totalPages = $state(0);
|
||||
let scale = $state(1.2);
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
let rendering = $state(false);
|
||||
|
||||
async function loadPdf() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
// Load PDF via axios to include auth token
|
||||
const path = pdfUrl.split('/download/')[1];
|
||||
const response = await api.get(`/download/${path}`, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
const blob = new Blob([response.data], { type: 'application/pdf' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const loadingTask = pdfjsLib.getDocument(url);
|
||||
pdfDoc = await loadingTask.promise;
|
||||
totalPages = pdfDoc.numPages;
|
||||
await renderPage(currentPage);
|
||||
} catch (err) {
|
||||
console.error('Error loading PDF:', err);
|
||||
error = 'Erreur lors du chargement du PDF';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPage(pageNum: number) {
|
||||
if (!pdfDoc || rendering) return;
|
||||
rendering = true;
|
||||
|
||||
try {
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale });
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (!context) return;
|
||||
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport
|
||||
}).promise;
|
||||
} catch (err) {
|
||||
console.error('Error rendering page:', err);
|
||||
} finally {
|
||||
rendering = false;
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
renderPage(currentPage);
|
||||
}
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
renderPage(currentPage);
|
||||
}
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
scale = Math.min(scale + 0.2, 3);
|
||||
renderPage(currentPage);
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
scale = Math.max(scale - 0.2, 0.5);
|
||||
renderPage(currentPage);
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
prevPage();
|
||||
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||
nextPage();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadPdf();
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
if (pdfDoc) {
|
||||
pdfDoc.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (pdfUrl) {
|
||||
loadPdf();
|
||||
}
|
||||
});
|
||||
</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 items-center gap-2">
|
||||
<span class="font-semibold">{title}</span>
|
||||
{#if totalPages > 0}
|
||||
<span class="text-gray-400">| Page {currentPage} / {totalPages}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={zoomOut}
|
||||
class="px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded text-sm"
|
||||
title="Zoom -"
|
||||
>
|
||||
➖
|
||||
</button>
|
||||
<span class="text-sm w-16 text-center">{Math.round(scale * 100)}%</span>
|
||||
<button
|
||||
onclick={zoomIn}
|
||||
class="px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded text-sm"
|
||||
title="Zoom +"
|
||||
>
|
||||
➕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto bg-gray-800 flex items-start justify-center p-4">
|
||||
{#if loading}
|
||||
<div class="text-white">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto mb-4"></div>
|
||||
<p>Chargement du PDF...</p>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="text-red-400 text-center">
|
||||
<p class="text-xl mb-2">⚠️</p>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<canvas bind:this={canvas} class="shadow-2xl"></canvas>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="bg-ohmj-dark text-white px-4 py-3 flex items-center justify-center gap-4">
|
||||
<button
|
||||
onclick={prevPage}
|
||||
disabled={currentPage <= 1}
|
||||
class="px-4 py-2 bg-ohmj-primary hover:bg-ohmj-secondary disabled:opacity-50 disabled:cursor-not-allowed rounded transition-colors"
|
||||
>
|
||||
◀ Précédent
|
||||
</button>
|
||||
|
||||
<div class="flex gap-1">
|
||||
{#each Array(Math.min(5, totalPages)) as _, i}
|
||||
{@const pageNum = Math.max(1, Math.min(currentPage - 2, totalPages - 4)) + i}
|
||||
{#if pageNum <= totalPages}
|
||||
<button
|
||||
onclick={() => { currentPage = pageNum; renderPage(pageNum); }}
|
||||
class="w-8 h-8 rounded {currentPage === pageNum ? 'bg-ohmj-secondary text-ohmj-dark font-bold' : 'bg-gray-700 hover:bg-gray-600'}"
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick={nextPage}
|
||||
disabled={currentPage >= totalPages}
|
||||
class="px-4 py-2 bg-ohmj-primary hover:bg-ohmj-secondary disabled:opacity-50 disabled:cursor-not-allowed rounded transition-colors"
|
||||
>
|
||||
Suivant ▶
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
43
partitions/src/lib/stores/auth.ts
Normal file
43
partitions/src/lib/stores/auth.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
interface User {
|
||||
username: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
user: User | null;
|
||||
}
|
||||
|
||||
function createAuthStore() {
|
||||
const stored = browser ? localStorage.getItem('auth') : null;
|
||||
const initial: AuthState = stored ? JSON.parse(stored) : { token: null, user: null };
|
||||
|
||||
const { subscribe, set, update } = writable<AuthState>(initial);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
login: (token: string, user: User) => {
|
||||
const state = { token, user };
|
||||
set(state);
|
||||
if (browser) {
|
||||
localStorage.setItem('auth', JSON.stringify(state));
|
||||
}
|
||||
},
|
||||
logout: () => {
|
||||
set({ token: null, user: null });
|
||||
if (browser) {
|
||||
localStorage.removeItem('auth');
|
||||
}
|
||||
},
|
||||
getToken: () => {
|
||||
let token: string | null = null;
|
||||
update((s) => (token = s.token));
|
||||
return token;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const auth = createAuthStore();
|
||||
74
partitions/src/routes/+layout.svelte
Normal file
74
partitions/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { auth } from '$lib/stores/auth';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const publicRoutes = ['/'];
|
||||
|
||||
let isAuthenticated = false;
|
||||
let currentPath = '/';
|
||||
|
||||
auth.subscribe((state) => {
|
||||
isAuthenticated = !!state.token;
|
||||
});
|
||||
|
||||
page.subscribe(($page) => {
|
||||
currentPath = $page.url.pathname;
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = page.subscribe(($page) => {
|
||||
const path = $page.url.pathname;
|
||||
const isPublic = publicRoutes.includes(path);
|
||||
|
||||
if (!isPublic && !isAuthenticated) {
|
||||
goto('/');
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
});
|
||||
|
||||
function logout() {
|
||||
auth.logout();
|
||||
goto('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen flex flex-col">
|
||||
{#if isAuthenticated && currentPath !== '/'}
|
||||
<header class="bg-ohmj-primary text-white shadow-lg">
|
||||
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
|
||||
<a href="/scores" class="flex items-center gap-3">
|
||||
<img src="/logo.png" alt="OHMJ" class="h-10" />
|
||||
<span class="text-xl font-bold text-ohmj-secondary hidden md:inline">
|
||||
Partitions
|
||||
</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"
|
||||
>
|
||||
Déconnexion
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
<main class="flex-1">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
{#if isAuthenticated && currentPath !== '/'}
|
||||
<footer class="bg-ohmj-dark text-gray-400 py-4 text-center text-sm">
|
||||
<p>© {new Date().getFullYear()} Harmonie de Montpellier-Jacou</p>
|
||||
</footer>
|
||||
{/if}
|
||||
</div>
|
||||
126
partitions/src/routes/+page.svelte
Normal file
126
partitions/src/routes/+page.svelte
Normal file
@@ -0,0 +1,126 @@
|
||||
<script lang="ts">
|
||||
import { auth } from '$lib/stores/auth';
|
||||
import { apiService } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let username = '';
|
||||
let password = '';
|
||||
let error = '';
|
||||
let loading = false;
|
||||
|
||||
async function handleLogin() {
|
||||
if (!username || !password) {
|
||||
error = 'Veuillez entrer un nom d\'utilisateur et un mot de passe';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const result = await apiService.login(username, password);
|
||||
auth.login(result.token, result.user);
|
||||
goto('/scores');
|
||||
} catch (err: any) {
|
||||
if (err.response?.status === 401) {
|
||||
error = 'Nom d\'utilisateur ou mot de passe incorrect';
|
||||
} else {
|
||||
error = 'Erreur de connexion au serveur';
|
||||
}
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
handleLogin();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Connexion - OHMJ</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen flex items-center justify-center relative overflow-hidden">
|
||||
<!-- Background image with blur -->
|
||||
<div class="absolute inset-0 z-0">
|
||||
<img src="/bg-login.jpg" alt="Background" class="w-full h-full object-cover" />
|
||||
<div class="absolute inset-0 bg-ohmj-primary/50 backdrop-blur-sm"></div>
|
||||
</div>
|
||||
|
||||
<div class="w-full max-w-md px-4 z-10">
|
||||
<div class="bg-white rounded-lg shadow-2xl p-8">
|
||||
<div class="text-center mb-8">
|
||||
<img src="/logo.png" alt="OHMJ" class="h-24 mx-auto mb-4" />
|
||||
<p class="text-gray-600">
|
||||
Harmonie de Montpellier-Jacou
|
||||
</p>
|
||||
<p class="text-gray-500 text-sm mt-2">
|
||||
Connexion à l'espace partitions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleLogin(); }} class="space-y-6">
|
||||
{#if error}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="username" class="block text-gray-700 text-sm font-bold mb-2">
|
||||
Nom d'utilisateur
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
bind:value={username}
|
||||
onkeydown={handleKeydown}
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-ohmj-primary focus:border-transparent"
|
||||
placeholder="musicien"
|
||||
autocomplete="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-gray-700 text-sm font-bold mb-2">
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
bind:value={password}
|
||||
onkeydown={handleKeydown}
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-ohmj-primary focus:border-transparent"
|
||||
placeholder="••••••••"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="w-full bg-ohmj-primary hover:bg-ohmj-dark text-white font-bold py-3 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{#if loading}
|
||||
<span class="inline-flex items-center">
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Connexion...
|
||||
</span>
|
||||
{:else}
|
||||
Se connecter
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="text-center text-gray-400 text-xs mt-6">
|
||||
Accès réservé aux membres de l'OHMJ
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
115
partitions/src/routes/scores/+page.svelte
Normal file
115
partitions/src/routes/scores/+page.svelte
Normal file
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { apiService, type Score } from '$lib/api';
|
||||
|
||||
let scores: Score[] = [];
|
||||
let loading = true;
|
||||
let error = '';
|
||||
let sortBy: 'id' | 'name' | 'compositor' = 'id';
|
||||
let sortOrder: 'asc' | 'desc' = 'asc';
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
scores = await apiService.getScores();
|
||||
} catch (err) {
|
||||
error = 'Erreur lors du chargement des partitions';
|
||||
console.error(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
function sortScores(key: 'id' | 'name' | 'compositor') {
|
||||
if (sortBy === key) {
|
||||
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortBy = key;
|
||||
sortOrder = 'asc';
|
||||
}
|
||||
}
|
||||
|
||||
$: sortedScores = [...scores].sort((a, b) => {
|
||||
let aVal = a[sortBy] || '';
|
||||
let bVal = b[sortBy] || '';
|
||||
if (sortBy === 'id') {
|
||||
aVal = a.id;
|
||||
bVal = b.id;
|
||||
}
|
||||
const cmp = aVal.localeCompare(bVal);
|
||||
return sortOrder === 'asc' ? cmp : -cmp;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Mes Partitions - 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>
|
||||
<p class="text-gray-600 mt-2">{scores.length} partitions disponibles</p>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-ohmj-primary"></div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<table class="min-w-full">
|
||||
<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"
|
||||
onclick={() => sortScores('id')}
|
||||
>
|
||||
№ {sortBy === 'id' ? (sortOrder === 'asc' ? '↑' : '↓') : ''}
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
onclick={() => sortScores('name')}
|
||||
>
|
||||
Nom {sortBy === 'name' ? (sortOrder === 'asc' ? '↑' : '↓') : ''}
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
onclick={() => sortScores('compositor')}
|
||||
>
|
||||
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">
|
||||
<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">
|
||||
{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>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
148
partitions/src/routes/scores/[id]/+page.svelte
Normal file
148
partitions/src/routes/scores/[id]/+page.svelte
Normal file
@@ -0,0 +1,148 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { apiService, type Score, type Piece } from '$lib/api';
|
||||
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: '🎵',
|
||||
pia: '🎹', sup: '📄', par: '📄'
|
||||
};
|
||||
|
||||
const INSTRUMENT_NAMES: Record<string, string> = {
|
||||
dir: 'Direction', pic: 'Piccolo', flu: 'Flûte', cla: 'Clarinette',
|
||||
clb: 'Clarinette Basse', sax: 'Sax Alto', sat: 'Sax Ténor', sab: 'Sax Baryton',
|
||||
coa: 'Cor Anglais', cba: 'Contrebasse', cor: 'Cor', trp: 'Trompette',
|
||||
trb: 'Trombone', tub: 'Tuba', htb: 'Hautbois', bas: 'Basson',
|
||||
per: 'Percussions', crn: 'Cornet', eup: 'Euphonium', har: 'Harpe',
|
||||
pia: 'Piano', sup: 'Parties supplementaires', par: 'Parties'
|
||||
};
|
||||
|
||||
function getInstrumentIcon(code: string): string {
|
||||
return INSTRUMENT_ICONS[code] || '🎵';
|
||||
}
|
||||
|
||||
function getInstrumentName(code: string): string {
|
||||
return INSTRUMENT_NAMES[code] || code;
|
||||
}
|
||||
|
||||
let scoreId = '';
|
||||
let score: Score | null = null;
|
||||
let pieces: Piece[] = [];
|
||||
let instruments: any[] = [];
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
let selectedPiece = $state(1);
|
||||
|
||||
page.subscribe(($page) => {
|
||||
scoreId = $page.params.id || '';
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
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 || [];
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Erreur lors du chargement';
|
||||
console.error(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function selectPiece(pieceId: number) {
|
||||
goto(`/scores/${scoreId}/${pieceId}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{score?.name || 'Partition'} - OHMJ</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<a href="/scores" class="inline-flex items-center text-ohmj-primary hover:underline mb-4">
|
||||
← Retour aux partitions
|
||||
</a>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-ohmj-primary"></div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold text-ohmj-primary">{score?.name || ''}</h1>
|
||||
{#if score?.compositor}
|
||||
<p class="text-gray-600 mt-1 text-lg">{score.compositor}</p>
|
||||
{/if}
|
||||
{#if score?.ressource}
|
||||
<a
|
||||
href={score.ressource}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center mt-2 text-ohmj-primary hover:underline"
|
||||
>
|
||||
🔗 Ressource externe
|
||||
</a>
|
||||
{/if}
|
||||
<p class="text-sm text-gray-400 mt-1">№ {scoreId}</p>
|
||||
</div>
|
||||
|
||||
{#if pieces.length > 1}
|
||||
<h2 class="text-xl font-semibold text-gray-700 mb-4">Pièces</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-8">
|
||||
{#each pieces as piece}
|
||||
<button
|
||||
onclick={() => selectPiece(piece.id)}
|
||||
class="bg-white p-4 rounded-lg shadow hover:shadow-lg transition-shadow border-2 {selectedPiece === piece.id ? 'border-ohmj-primary' : 'border-transparent hover:border-ohmj-primary'}"
|
||||
>
|
||||
<p class="font-semibold text-gray-800">{piece.name}</p>
|
||||
<p class="text-sm text-gray-500">№ {piece.id}</p>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if pieces.length <= 1 || selectedPiece}
|
||||
<h2 class="text-xl font-semibold text-gray-700 mb-4">Instruments disponibles</h2>
|
||||
|
||||
{#if instruments.length === 0}
|
||||
<p class="text-gray-500">Aucun instrument trouvé.</p>
|
||||
{:else}
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{#each instruments as instrument}
|
||||
{@const code = instrument.id || instrument.code || ''}
|
||||
{@const title = instrument.title || getInstrumentName(code)}
|
||||
{@const fileCount = instrument.parts?.[0]?.files?.length || 0}
|
||||
<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>
|
||||
<h3 class="font-semibold text-gray-800">{title}</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">{fileCount} fichier{fileCount !== 1 ? 's' : ''}</p>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
79
partitions/src/routes/scores/[id]/[piece]/+page.svelte
Normal file
79
partitions/src/routes/scores/[id]/[piece]/+page.svelte
Normal file
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
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: '🎵',
|
||||
pia: '🎹', sup: '📄', par: '📄'
|
||||
};
|
||||
|
||||
function getInstrumentIcon(code: string): string {
|
||||
return INSTRUMENT_ICONS[code] || '🎵';
|
||||
}
|
||||
|
||||
let scoreId = '';
|
||||
let pieceId = '';
|
||||
let instruments: Instrument[] = $state([]);
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
|
||||
page.subscribe(($page) => {
|
||||
scoreId = $page.params.id || '';
|
||||
pieceId = $page.params.piece || '';
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const score = await apiService.getScore(scoreId);
|
||||
// Filter instruments by piece
|
||||
instruments = (score.instruments || []).filter(i => i.piece === pieceId);
|
||||
} catch (err) {
|
||||
error = 'Erreur lors du chargement';
|
||||
console.error(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Pièce {pieceId} - {scoreId} - OHMJ</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<a href="/scores/{scoreId}" class="inline-flex items-center text-ohmj-primary hover:underline mb-4">
|
||||
← Retour aux pièces
|
||||
</a>
|
||||
|
||||
<h1 class="text-2xl font-bold text-ohmj-primary mb-6">Instruments - Pièce {pieceId}</h1>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-ohmj-primary"></div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
{:else}
|
||||
<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}
|
||||
<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>
|
||||
<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>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,190 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { apiService, type Part, type PdfFile } from '$lib/api';
|
||||
import PdfViewer from '$lib/components/PdfViewer.svelte';
|
||||
|
||||
const INSTRUMENT_NAMES: Record<string, string> = {
|
||||
dir: 'Direction',
|
||||
pic: 'Piccolo',
|
||||
flu: 'Flûte',
|
||||
cla: 'Clarinette',
|
||||
clb: 'Clarinette basse',
|
||||
sax: 'Saxophone',
|
||||
sab: 'Saxophone baryton',
|
||||
sat: 'Saxophone ténor',
|
||||
cba: 'Contrebasse',
|
||||
cor: 'Cor',
|
||||
trp: 'Trompette',
|
||||
trb: 'Trombone',
|
||||
tub: 'Tuba',
|
||||
htb: 'Hautbois',
|
||||
bas: 'Basson',
|
||||
per: 'Percussion',
|
||||
crn: 'Cornet',
|
||||
eup: 'Euphonium',
|
||||
har: 'Harpe',
|
||||
pia: 'Piano'
|
||||
};
|
||||
|
||||
function getInstrumentName(code: string): string {
|
||||
return INSTRUMENT_NAMES[code] || code;
|
||||
}
|
||||
|
||||
let scoreId = '';
|
||||
let pieceId = '';
|
||||
let instrumentCode = '';
|
||||
let instrumentTitle = '';
|
||||
let parts: Part[] = $state([]);
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
let selectedFile = $state('');
|
||||
let showSidebar = $state(true);
|
||||
|
||||
let pdfViewerUrl = $derived(selectedFile ? apiService.getDownloadUrl(selectedFile) : '');
|
||||
let pdfFileName = $derived(selectedFile ? selectedFile.split('/').pop() || 'Partition' : 'Partition');
|
||||
|
||||
page.subscribe(($page) => {
|
||||
scoreId = $page.params.id || '';
|
||||
pieceId = $page.params.piece || '';
|
||||
instrumentCode = $page.params.instrument || '';
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const score = await apiService.getScore(scoreId);
|
||||
const instrument = (score.instruments || []).find(
|
||||
i => i.id === instrumentCode && i.piece === pieceId
|
||||
);
|
||||
if (instrument) {
|
||||
instrumentTitle = instrument.title;
|
||||
parts = instrument.parts || [];
|
||||
}
|
||||
if (parts.length > 0 && parts[0].files.length > 0) {
|
||||
selectedFile = parts[0].files[0].path;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Erreur lors du chargement des parties';
|
||||
console.error(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
function getFileInfo(file: PdfFile): string {
|
||||
const info = [];
|
||||
if (file.part) info.push(`Partie ${file.part}`);
|
||||
if (file.key) info.push(file.key);
|
||||
if (file.clef) info.push(file.clef);
|
||||
if (file.variant) info.push(file.variant);
|
||||
return info.length > 0 ? `(${info.join(', ')})` : '';
|
||||
}
|
||||
|
||||
async function downloadFile(path: string) {
|
||||
try {
|
||||
const blob = await apiService.downloadPdf(path);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = path.split('/').pop() || 'partition.pdf';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
} catch (err) {
|
||||
console.error('Error downloading:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function openViewer(path: string) {
|
||||
selectedFile = path;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{instrumentTitle || instrumentCode} - {scoreId} - OHMJ</title>
|
||||
</svelte:head>
|
||||
|
||||
<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">
|
||||
← Retour aux instruments
|
||||
</a>
|
||||
|
||||
<h1 class="text-2xl font-bold text-ohmj-primary">
|
||||
{instrumentTitle || getInstrumentName(instrumentCode)} - Partition {scoreId} - Pièce {pieceId}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-ohmj-primary"></div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-1 flex overflow-hidden relative">
|
||||
<!-- Toggle button -->
|
||||
<button
|
||||
onclick={() => showSidebar = !showSidebar}
|
||||
class="absolute left-0 top-1/2 transform -translate-y-1/2 z-10 bg-ohmj-primary text-white px-2 py-4 rounded-r shadow hover:bg-ohmj-secondary transition-colors"
|
||||
title={showSidebar ? 'Masquer' : 'Afficher'}
|
||||
>
|
||||
{showSidebar ? '◀' : '▶'}
|
||||
</button>
|
||||
|
||||
<!-- Liste des parties -->
|
||||
{#if showSidebar}
|
||||
<div class="w-64 min-w-[200px] max-w-xs bg-white border-r overflow-y-auto p-4 flex-shrink-0 ml-8">
|
||||
<h2 class="font-semibold text-gray-700 mb-4">Parties disponibles</h2>
|
||||
|
||||
{#each parts as part}
|
||||
<div class="mb-4">
|
||||
<h3 class="font-medium text-gray-800 mb-2">Version {part.id}</h3>
|
||||
<ul class="space-y-1">
|
||||
{#each part.files as file}
|
||||
<li class="flex items-center gap-2">
|
||||
<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'}"
|
||||
>
|
||||
📖 {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"
|
||||
title="Télécharger"
|
||||
>
|
||||
⬇️
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if parts.length === 0}
|
||||
<p class="text-gray-500">Aucune partie disponible.</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Visionneuse PDF -->
|
||||
<div class="flex-1 bg-gray-800 flex flex-col ml-8">
|
||||
{#if selectedFile}
|
||||
{#key selectedFile}
|
||||
<PdfViewer pdfUrl={pdfViewerUrl} title={pdfFileName} />
|
||||
{/key}
|
||||
{:else}
|
||||
<div class="h-full flex items-center justify-center text-gray-400">
|
||||
<p>Sélectionnez une partition pour la visionner</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
77
partitions/src/routes/test-icons/+page.svelte
Normal file
77
partitions/src/routes/test-icons/+page.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<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: '🎵',
|
||||
pia: '🎹',
|
||||
sup: '📄',
|
||||
par: '📄'
|
||||
};
|
||||
|
||||
const INSTRUMENT_NAMES: Record<string, string> = {
|
||||
dir: 'Direction',
|
||||
pic: 'Piccolo',
|
||||
flu: 'Flûte',
|
||||
cla: 'Clarinette',
|
||||
clb: 'Clarinette Basse',
|
||||
sax: 'Sax Alto',
|
||||
sat: 'Sax Ténor',
|
||||
sab: 'Sax Baryton',
|
||||
coa: 'Cor Anglais',
|
||||
cba: 'Contrebasse',
|
||||
cor: 'Cor',
|
||||
trp: 'Trompette',
|
||||
trb: 'Trombone',
|
||||
tub: 'Tuba',
|
||||
htb: 'Hautbois',
|
||||
bas: 'Basson',
|
||||
per: 'Percussions',
|
||||
crn: 'Cornet',
|
||||
eup: 'Euphonium',
|
||||
har: 'Harpe',
|
||||
pia: 'Piano',
|
||||
sup: 'Parties supplementaires',
|
||||
par: 'Parties'
|
||||
};
|
||||
|
||||
const instruments = Object.entries(INSTRUMENT_NAMES).map(([code, name]) => ({
|
||||
code,
|
||||
name,
|
||||
icon: INSTRUMENT_ICONS[code] || '❓'
|
||||
}));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Test icônes instruments - OHMJ</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold text-ohmj-primary mb-8">Test icônes instruments</h1>
|
||||
|
||||
<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>
|
||||
<p class="font-semibold">{inst.name}</p>
|
||||
<p class="text-sm text-gray-400">{inst.code}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
BIN
partitions/static/bg-login.jpg
Normal file
BIN
partitions/static/bg-login.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 315 KiB |
BIN
partitions/static/logo.png
Normal file
BIN
partitions/static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
21
partitions/static/pdf.worker.min.mjs
Normal file
21
partitions/static/pdf.worker.min.mjs
Normal file
File diff suppressed because one or more lines are too long
21
partitions/svelte.config.js
Normal file
21
partitions/svelte.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: 'index.html',
|
||||
precompress: false,
|
||||
strict: true
|
||||
}),
|
||||
alias: {
|
||||
$lib: './src/lib'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
17
partitions/tailwind.config.js
Normal file
17
partitions/tailwind.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
ohmj: {
|
||||
primary: '#1e3a5f',
|
||||
secondary: '#c9a227',
|
||||
light: '#f5f5f5',
|
||||
dark: '#0a1f33'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
};
|
||||
14
partitions/tsconfig.json
Normal file
14
partitions/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
||||
9
partitions/vite.config.ts
Normal file
9
partitions/vite.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
server: {
|
||||
port: 5173
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user