chore: initial commit — monorepo ReadaBook (API NestJS, web PWA, Docker)

This commit is contained in:
Git Agent
2026-08-23 09:56:53 +02:00
commit 8f1140127f
79 changed files with 6456 additions and 0 deletions

View File

@ -0,0 +1,107 @@
import { FormEvent, useEffect, useState } from "react";
import { Play, Plus } from "lucide-react";
import type { JobDto, LibraryDto, UserDto } from "@readabook/shared";
import { api } from "../api/client";
import { ErrorRibbon, LoadingState, Panel } from "../components/ui";
export function AdminPage() {
const [libraries, setLibraries] = useState<LibraryDto[] | null>(null);
const [jobs, setJobs] = useState<JobDto[]>([]);
const [users, setUsers] = useState<UserDto[]>([]);
const [name, setName] = useState("Bibliotheque locale");
const [path, setPath] = useState("/library");
const [error, setError] = useState<string>();
async function refresh() {
const [nextLibraries, nextJobs, nextUsers] = await Promise.all([api.libraries(), api.jobs(), api.users()]);
setLibraries(nextLibraries);
setJobs(nextJobs);
setUsers(nextUsers);
}
useEffect(() => {
refresh().catch((refreshError) => setError(refreshError instanceof Error ? refreshError.message : "Administration indisponible"));
}, []);
async function createLibrary(event: FormEvent) {
event.preventDefault();
setError(undefined);
try {
await api.createLibrary({ name, path, enabled: true });
await refresh();
} catch (createError) {
setError(createError instanceof Error ? createError.message : "Creation impossible");
}
}
async function scan(id: number) {
setError(undefined);
try {
await api.scanLibrary(id);
await refresh();
} catch (scanError) {
setError(scanError instanceof Error ? scanError.message : "Scan impossible");
}
}
if (!libraries) return <LoadingState />;
return (
<div className="page-grid">
<Panel className="span-2">
<div className="section-heading">
<h1>Administration</h1>
<span>{users.length} comptes</span>
</div>
<ErrorRibbon message={error} />
<form className="admin-form" onSubmit={createLibrary}>
<label>
Nom du rayon
<input value={name} onChange={(event) => setName(event.target.value)} required />
</label>
<label>
Chemin serveur
<input value={path} onChange={(event) => setPath(event.target.value)} required />
</label>
<button className="primary-button" type="submit">
<Plus size={17} />
Ajouter
</button>
</form>
</Panel>
<Panel>
<div className="section-heading">
<h2>Travaux</h2>
<span>{jobs.length}</span>
</div>
<div className="job-list">
{jobs.map((job) => (
<div key={job.id}>
<strong>{job.type}</strong>
<span>{job.status}</span>
</div>
))}
</div>
</Panel>
<Panel className="span-3">
<div className="library-table">
{libraries.map((library) => (
<div key={library.id}>
<div>
<strong>{library.name}</strong>
<span>{library.path}</span>
</div>
<span>{library.enabled ? "actif" : "pause"}</span>
<button className="ghost-button" onClick={() => scan(library.id)}>
<Play size={16} />
Scanner
</button>
</div>
))}
</div>
</Panel>
</div>
);
}