chore: initial commit — monorepo ReadaBook (API NestJS, web PWA, Docker)
This commit is contained in:
140
apps/web/src/api/client.ts
Normal file
140
apps/web/src/api/client.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import type {
|
||||
BookDto,
|
||||
BookQueryDto,
|
||||
BootstrapAdminDto,
|
||||
CreateLibraryDto,
|
||||
JobDto,
|
||||
LibraryDto,
|
||||
LoginDto,
|
||||
ProgressDto,
|
||||
UpdateProgressDto,
|
||||
UserDto
|
||||
} from "@readabook/shared";
|
||||
import { mockBooks, mockContinue, mockJobs, mockLibraries, mockProgress, mockUser } from "./mockData";
|
||||
import type { ContinueItem, Session } from "./types";
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "";
|
||||
|
||||
type RequestOptions = RequestInit & {
|
||||
fallback?: unknown;
|
||||
};
|
||||
|
||||
export class ApiFallbackError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly fallback: unknown
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
...options,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new Error(detail || `${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
} catch (error) {
|
||||
if (options.fallback !== undefined) {
|
||||
throw new ApiFallbackError(error instanceof Error ? error.message : String(error), options.fallback);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function queryString(query: Partial<BookQueryDto>): string {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== "") params.set(key, String(value));
|
||||
});
|
||||
const value = params.toString();
|
||||
return value ? `?${value}` : "";
|
||||
}
|
||||
|
||||
export const api = {
|
||||
async session(): Promise<Session> {
|
||||
try {
|
||||
const result = await request<{ user: UserDto }>("/auth/me");
|
||||
return { user: result.user, degraded: false };
|
||||
} catch {
|
||||
return { user: null, degraded: false };
|
||||
}
|
||||
},
|
||||
async bootstrap(input: BootstrapAdminDto): Promise<UserDto> {
|
||||
const result = await request<UserDto>("/auth/bootstrap", { method: "POST", body: JSON.stringify(input) });
|
||||
return result;
|
||||
},
|
||||
async login(input: LoginDto): Promise<UserDto> {
|
||||
const result = await request<{ user: UserDto }>("/auth/login", { method: "POST", body: JSON.stringify(input) });
|
||||
return result.user;
|
||||
},
|
||||
async logout(): Promise<void> {
|
||||
await request<{ ok: true }>("/auth/logout", { method: "POST" });
|
||||
},
|
||||
async books(query: Partial<BookQueryDto> = {}): Promise<BookDto[]> {
|
||||
return request<BookDto[]>(`/books${queryString({ limit: 50, offset: 0, ...query })}`, { fallback: mockBooks });
|
||||
},
|
||||
async search(query: string): Promise<BookDto[]> {
|
||||
return request<BookDto[]>(`/books/search${queryString({ q: query, limit: 50, offset: 0 })}`, { fallback: mockBooks });
|
||||
},
|
||||
async book(id: number): Promise<BookDto> {
|
||||
const fallback = mockBooks.find((book) => book.id === id) ?? mockBooks[0];
|
||||
return request<BookDto>(`/books/${id}`, { fallback });
|
||||
},
|
||||
bookFileUrl(id: number): string {
|
||||
return `${API_BASE}/books/${id}/file`;
|
||||
},
|
||||
bookCoverUrl(id: number): string {
|
||||
return `${API_BASE}/books/${id}/cover`;
|
||||
},
|
||||
async progress(bookId: number): Promise<ProgressDto | null> {
|
||||
try {
|
||||
return await request<ProgressDto>(`/progress/${bookId}`, {
|
||||
fallback: mockProgress.find((progress) => progress.bookId === bookId) ?? null
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ApiFallbackError) return error.fallback as ProgressDto | null;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async saveProgress(bookId: number, input: UpdateProgressDto): Promise<ProgressDto> {
|
||||
return request<ProgressDto>(`/progress/${bookId}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(input),
|
||||
fallback: { bookId, ...input, updatedAt: new Date().toISOString() }
|
||||
});
|
||||
},
|
||||
async continueReading(): Promise<ContinueItem[]> {
|
||||
return request<ContinueItem[]>("/progress/continue", { fallback: mockContinue });
|
||||
},
|
||||
async libraries(): Promise<LibraryDto[]> {
|
||||
return request<LibraryDto[]>("/admin/libraries", { fallback: mockLibraries });
|
||||
},
|
||||
async createLibrary(input: CreateLibraryDto): Promise<LibraryDto> {
|
||||
return request<LibraryDto>("/admin/libraries", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
fallback: { id: Date.now(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), ...input }
|
||||
});
|
||||
},
|
||||
async scanLibrary(id: number): Promise<JobDto> {
|
||||
return request<JobDto>(`/admin/libraries/${id}/scan`, { method: "POST", fallback: mockJobs[0] });
|
||||
},
|
||||
async jobs(): Promise<JobDto[]> {
|
||||
return request<JobDto[]>("/admin/jobs", { fallback: mockJobs });
|
||||
},
|
||||
async users(): Promise<UserDto[]> {
|
||||
return request<UserDto[]>("/admin/users", { fallback: [mockUser] });
|
||||
}
|
||||
};
|
||||
70
apps/web/src/api/mockData.ts
Normal file
70
apps/web/src/api/mockData.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import type { BookDto, JobDto, LibraryDto, ProgressDto, UserDto } from "@readabook/shared";
|
||||
import type { ContinueItem } from "./types";
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
export const mockUser: UserDto = {
|
||||
id: 1,
|
||||
email: "admin@readabook.local",
|
||||
name: "Conservateur",
|
||||
role: "admin",
|
||||
createdAt: now
|
||||
};
|
||||
|
||||
export const mockLibraries: LibraryDto[] = [
|
||||
{ id: 1, name: "Reserve des EPUB", path: "/library/epub", enabled: true, createdAt: now, updatedAt: now },
|
||||
{ id: 2, name: "Atlas PDF", path: "/library/pdf", enabled: true, createdAt: now, updatedAt: now }
|
||||
];
|
||||
|
||||
export const mockBooks: BookDto[] = [
|
||||
{
|
||||
id: 1,
|
||||
libraryId: 1,
|
||||
title: "L'Herbier des machines",
|
||||
author: "M. Valrose",
|
||||
description: "Fragments, croquis et notes rassemblees autour d'automates introuvables.",
|
||||
isbn: null,
|
||||
language: "fr",
|
||||
publisher: "Cabinet ReadaBook",
|
||||
publishedDate: "1908",
|
||||
format: "epub",
|
||||
filePath: "/library/epub/herbier.epub",
|
||||
coverPath: null,
|
||||
fileSize: 4300000,
|
||||
fileMtime: now,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
libraryId: 2,
|
||||
title: "Cartographie des songes",
|
||||
author: "I. Nadir",
|
||||
description: "Un atlas annote ou chaque page devient une vitrine de lecture.",
|
||||
isbn: null,
|
||||
language: "fr",
|
||||
publisher: "ReadaBook",
|
||||
publishedDate: "1921",
|
||||
format: "pdf",
|
||||
filePath: "/library/pdf/cartographie.pdf",
|
||||
coverPath: null,
|
||||
fileSize: 9100000,
|
||||
fileMtime: now,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
];
|
||||
|
||||
export const mockProgress: ProgressDto[] = [
|
||||
{ bookId: 1, locator: "mock:chapter-3", percent: 42, updatedAt: now },
|
||||
{ bookId: 2, locator: "mock:page-12", percent: 18, updatedAt: now }
|
||||
];
|
||||
|
||||
export const mockContinue: ContinueItem[] = mockProgress.map((progress) => ({
|
||||
progress,
|
||||
book: mockBooks.find((book) => book.id === progress.bookId) ?? mockBooks[0]
|
||||
}));
|
||||
|
||||
export const mockJobs: JobDto[] = [
|
||||
{ id: 1, type: "scan-library", status: "succeeded", detail: "2 ouvrages indexes", error: null, createdAt: now, updatedAt: now }
|
||||
];
|
||||
23
apps/web/src/api/types.ts
Normal file
23
apps/web/src/api/types.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import type { BookDto, JobDto, LibraryDto, ProgressDto, UserDto } from "@readabook/shared";
|
||||
|
||||
export type ApiState<T> =
|
||||
| { status: "loading"; data?: T; error?: undefined; fallback?: false }
|
||||
| { status: "ready"; data: T; error?: undefined; fallback?: boolean }
|
||||
| { status: "error"; data: T; error: string; fallback: true };
|
||||
|
||||
export type Session = {
|
||||
user: UserDto | null;
|
||||
degraded: boolean;
|
||||
};
|
||||
|
||||
export type ContinueItem = {
|
||||
book: BookDto;
|
||||
progress: ProgressDto;
|
||||
};
|
||||
|
||||
export type DashboardData = {
|
||||
books: BookDto[];
|
||||
continueReading: ContinueItem[];
|
||||
libraries: LibraryDto[];
|
||||
jobs: JobDto[];
|
||||
};
|
||||
Reference in New Issue
Block a user