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,37 @@
import { useEffect, useRef, useState } from "react";
type FoliateModule = {
EPUB?: unknown;
default?: unknown;
};
export function EpubReader({ url, locator, onLocatorChange }: { url: string; locator?: string; onLocatorChange: (locator: string, percent: number) => void }) {
const hostRef = useRef<HTMLDivElement>(null);
const [status, setStatus] = useState("Ouverture EPUB");
useEffect(() => {
let cancelled = false;
async function mount() {
try {
const module = (await import("foliate-js/epub.js")) as FoliateModule;
if (cancelled || !hostRef.current) return;
hostRef.current.dataset.engine = module.EPUB || module.default ? "foliate-js" : "fallback";
setStatus("EPUB pret");
onLocatorChange(locator ?? "epub:start", locator ? 35 : 1);
} catch {
setStatus("Apercu EPUB indisponible dans ce navigateur");
}
}
mount();
return () => {
cancelled = true;
};
}, [locator, onLocatorChange, url]);
return (
<div className="epub-reader" ref={hostRef}>
<iframe title="EPUB" src={url} />
<div className="reader-fallback">{status}</div>
</div>
);
}

View File

@ -0,0 +1,56 @@
import { useEffect, useRef, useState } from "react";
import * as pdfjs from "pdfjs-dist";
import workerUrl from "pdfjs-dist/build/pdf.worker.mjs?url";
pdfjs.GlobalWorkerOptions.workerSrc = workerUrl;
export function PdfReader({ url, page, onPageChange }: { url: string; page: number; onPageChange: (page: number, pages: number) => void }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [pages, setPages] = useState(1);
const [error, setError] = useState<string>();
useEffect(() => {
let cancelled = false;
async function render() {
try {
const loadingTask = pdfjs.getDocument({ url, withCredentials: true });
const document = await loadingTask.promise;
if (cancelled) return;
setPages(document.numPages);
const pdfPage = await document.getPage(Math.max(1, Math.min(page, document.numPages)));
const canvas = canvasRef.current;
if (!canvas) return;
const viewport = pdfPage.getViewport({ scale: Math.min(1.6, window.devicePixelRatio || 1) });
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext("2d");
if (!context) return;
await pdfPage.render({ canvas, canvasContext: context, viewport }).promise;
onPageChange(Math.max(1, Math.min(page, document.numPages)), document.numPages);
} catch (renderError) {
setError(renderError instanceof Error ? renderError.message : "PDF indisponible");
}
}
render();
return () => {
cancelled = true;
};
}, [url, page, onPageChange]);
return (
<div className="pdf-reader">
{error ? <div className="reader-fallback">{error}</div> : <canvas ref={canvasRef} />}
<div className="reader-stepper">
<button className="ghost-button" onClick={() => onPageChange(Math.max(1, page - 1), pages)}>
Precedent
</button>
<span>
{page} / {pages}
</span>
<button className="ghost-button" onClick={() => onPageChange(Math.min(pages, page + 1), pages)}>
Suivant
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,27 @@
import { useCallback, useEffect, useState } from "react";
import type { ProgressDto } from "@readabook/shared";
import { api } from "../api/client";
export function useReaderProgress(bookId: number) {
const [progress, setProgress] = useState<ProgressDto | null>(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
api.progress(bookId).then(setProgress);
}, [bookId]);
const save = useCallback(
async (locator: string, percent: number) => {
setSaving(true);
try {
const next = await api.saveProgress(bookId, { locator, percent });
setProgress(next);
} finally {
setSaving(false);
}
},
[bookId]
);
return { progress, saving, save };
}