From 39dd130c47b910997e29c11d14087ab6d3c75f17 Mon Sep 17 00:00:00 2001 From: Git Agent Date: Sun, 23 Aug 2026 11:28:07 +0200 Subject: [PATCH] =?UTF-8?q?fix(web):=20admin=20des=20biblioth=C3=A8ques=20?= =?UTF-8?q?=E2=80=94=20UX,=20gestion=20d'erreur,=20styles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - web: AdminPage (admin bibliothèques), client API, styles - web: nginx Refs: #15 --- apps/web/nginx/default.conf | 3 +++ apps/web/src/api/client.ts | 3 +++ apps/web/src/pages/AdminPage.tsx | 37 ++++++++++++++++++++++++++++---- apps/web/src/styles/app.css | 16 +++++++++++++- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/apps/web/nginx/default.conf b/apps/web/nginx/default.conf index b6d7bed..4061245 100644 --- a/apps/web/nginx/default.conf +++ b/apps/web/nginx/default.conf @@ -26,6 +26,9 @@ server { } location ^~ /admin { + if ($http_accept ~* "text/html") { + rewrite ^ /index.html last; + } proxy_pass http://api:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index 93825f2..c0f9e21 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -165,6 +165,9 @@ export const api = { fallback: { id: Date.now(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), ...input } }); }, + async deleteLibrary(id: number): Promise { + await request<{ ok: true }>(`/admin/libraries/${id}`, { method: "DELETE" }); + }, async scanLibrary(id: number): Promise { return request(`/admin/libraries/${id}/scan`, { method: "POST", fallback: mockJobs[0] }); }, diff --git a/apps/web/src/pages/AdminPage.tsx b/apps/web/src/pages/AdminPage.tsx index f737fd4..0990dc5 100644 --- a/apps/web/src/pages/AdminPage.tsx +++ b/apps/web/src/pages/AdminPage.tsx @@ -1,5 +1,5 @@ import { FormEvent, useEffect, useState } from "react"; -import { Play, Plus } from "lucide-react"; +import { Play, Plus, Trash2 } from "lucide-react"; import type { JobDto, LibraryDto, UserDto } from "@readabook/shared"; import { api, getApiFallback } from "../api/client"; import { EmptyState, ErrorRibbon, LoadingState, Panel } from "../components/ui"; @@ -12,6 +12,7 @@ export function AdminPage() { const [name, setName] = useState("Bibliotheque locale"); const [path, setPath] = useState("/library"); const [error, setError] = useState(); + const [success, setSuccess] = useState(); async function refresh() { setLoading(true); @@ -51,9 +52,11 @@ export function AdminPage() { async function createLibrary(event: FormEvent) { event.preventDefault(); setError(undefined); + setSuccess(undefined); try { await api.createLibrary({ name, path, enabled: true }); await refresh(); + setSuccess(`Bibliothèque "${name}" ajoutée.`); } catch (createError) { const fallback = getApiFallback(createError); if (fallback) setLibraries((current) => [fallback, ...current]); @@ -63,9 +66,11 @@ export function AdminPage() { async function scan(id: number) { setError(undefined); + setSuccess(undefined); try { await api.scanLibrary(id); await refresh(); + setSuccess("Scan demandé."); } catch (scanError) { const fallback = getApiFallback(scanError); if (fallback) setJobs((current) => [fallback, ...current]); @@ -73,6 +78,23 @@ export function AdminPage() { } } + async function deleteLibrary(library: LibraryDto) { + const confirmed = window.confirm( + `Supprimer la bibliothèque "${library.name}" ?\n\nLes livres restent sur le disque. ReadaBook supprimera seulement cette bibliothèque du catalogue.` + ); + if (!confirmed) return; + + setError(undefined); + setSuccess(undefined); + try { + await api.deleteLibrary(library.id); + setLibraries((current) => current.filter((item) => item.id !== library.id)); + setSuccess(`Bibliothèque "${library.name}" supprimée. Les fichiers disque n'ont pas été supprimés.`); + } catch (deleteError) { + setError(deleteError instanceof Error ? deleteError.message : "Suppression impossible"); + } + } + return (
@@ -81,6 +103,7 @@ export function AdminPage() { {loading ? "chargement" : `${users.length} comptes`}
+ {success &&
{success}
} {error && (
Les formulaires restent disponibles. @@ -91,12 +114,14 @@ export function AdminPage() { )}
+
))} diff --git a/apps/web/src/styles/app.css b/apps/web/src/styles/app.css index 554bd2f..fc48d90 100644 --- a/apps/web/src/styles/app.css +++ b/apps/web/src/styles/app.css @@ -142,6 +142,15 @@ h2 { border-color: rgba(213, 168, 77, 0.55); } +.danger-button { + color: #ffd8cf; +} + +.danger-button:hover { + border-color: rgba(169, 72, 52, 0.78); + background: rgba(169, 72, 52, 0.16); +} + .book-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); @@ -271,7 +280,7 @@ h2 { } .library-table > div { - grid-template-columns: minmax(0, 1fr) auto auto; + grid-template-columns: minmax(0, 1fr) auto auto auto; align-items: center; } @@ -327,6 +336,11 @@ label { font-size: 0.9rem; } +label small { + color: var(--ink-muted); + line-height: 1.35; +} + input { min-height: 42px; border: 1px solid var(--line);