diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..ce5665a
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,96 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+ERitors Scribe is an Electron + Next.js desktop application for writers. It uses SQLite with WASM for local storage, supports offline mode with PIN authentication, and encrypts user data with AES-256-CBC.
+
+## Commands
+
+```bash
+# Development (starts Next.js on port 4000 + Electron with hot reload)
+npm run dev
+
+# Build for specific platform
+npm run build:mac
+npm run build:win
+npm run build:linux
+npm run build:all
+```
+
+## Architecture
+
+### Process Model
+
+```
+Renderer (Next.js) <--IPC--> Main Process <--> SQLite DB
+ via window.electron.invoke()
+```
+
+- **Main process**: `electron/main.ts` - Window management, IPC handlers, OS integration
+- **Preload script**: `electron/preload.ts` - Secure contextBridge API
+- **Renderer**: Next.js App Router with static export (`output: 'export'`)
+
+### Key Directories
+
+- `electron/` - Main process code (IPC handlers, database, storage)
+- `electron/ipc/` - IPC handlers organized by domain (book, chapter, character, etc.)
+- `electron/database/` - SQLite service, models, repositories, encryption
+- `electron/storage/SecureStorage.ts` - OS-level secure storage (macOS Keychain, Windows DPAPI)
+- `app/` - Next.js pages (App Router)
+- `context/` - React contexts for state management (13+ contexts)
+- `components/` - React components
+- `lib/locales/` - i18n translations (fr.json, en.json)
+
+### IPC Handler Pattern
+
+All IPC handlers use `createHandler
()` factory in `electron/database/LocalSystem.ts`:
+
+```typescript
+// electron/ipc/book.ipc.ts
+ipcMain.handle('db:book:books', createHandler(
+ async (userId, _body, lang) => Book.getBooks(userId, lang)
+));
+
+// Frontend usage
+const books = await window.electron.invoke('db:book:books');
+```
+
+The factory auto-injects `userId` and `lang` from secure storage, handles errors uniformly.
+
+### State Management
+
+Context-based architecture with providers:
+- `UserContext` - Authenticated user
+- `BookContext` - Current book
+- `OfflineContext` - Offline mode state
+- `SessionContext` - Session data
+- `LangContext` - i18n
+
+### Database
+
+- One SQLite file per user: `eritors-local-{userId}.db`
+- Uses `node-sqlite3-wasm` (WASM-based SQLite)
+- AES-256-CBC encryption for stored data
+- PBKDF2 key derivation (100K iterations)
+- Schema in `electron/database/schema.ts`
+
+### Security
+
+- `contextIsolation: true`, `nodeIntegration: false`, `sandbox: true`
+- OS-level secure storage for tokens and encryption keys
+- Per-user encryption keys stored in SecureStorage
+- Offline PIN hashed with bcrypt
+
+## TypeScript Configuration
+
+- `tsconfig.json` - Next.js/React (renderer)
+- `tsconfig.electron.json` - Electron main process
+- `tsconfig.preload.json` - Preload script
+
+## Build Output
+
+- `dist/` - Compiled TypeScript
+- `out/` - Next.js static export
+- `release/` - Electron builder output (DMG, NSIS, AppImage)
diff --git a/components/book/ImportBookForm.tsx b/components/book/ImportBookForm.tsx
new file mode 100644
index 0000000..95a78b7
--- /dev/null
+++ b/components/book/ImportBookForm.tsx
@@ -0,0 +1,349 @@
+'use client'
+import {ChangeEvent, Dispatch, RefObject, SetStateAction, useCallback, useContext, useEffect, useRef, useState} from "react";
+import {AlertContext} from "@/context/AlertContext";
+import System from "@/lib/models/System";
+import {SessionContext} from "@/context/SessionContext";
+import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
+import {
+ faBook,
+ faBookOpen,
+ faBookmark,
+ faFileImport,
+ faFileWord,
+ faLayerGroup,
+ faSpinner,
+ faSquare,
+ faSquareCheck,
+ faX
+} from "@fortawesome/free-solid-svg-icons";
+import {SelectBoxProps} from "@/shared/interface";
+import {bookTypes} from "@/lib/models/Book";
+import {chapterVersions} from "@/lib/models/Chapter";
+import {ParsedDocxResponse, ImportChapterSelection} from "@/lib/models/Import";
+import {SyncedBook} from "@/lib/models/SyncedBook";
+import InputField from "@/components/form/InputField";
+import TextInput from "@/components/form/TextInput";
+import TexteAreaInput from "@/components/form/TexteAreaInput";
+import SelectBox from "@/components/form/SelectBox";
+import CancelButton from "@/components/form/CancelButton";
+import SubmitButtonWLoading from "@/components/form/SubmitButtonWLoading";
+import {useTranslations} from "next-intl";
+import {LangContext, LangContextProps} from "@/context/LangContext";
+import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
+
+const DOCX_ACCEPT: string = '.docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document';
+
+export default function ImportBookForm({setCloseForm}: { setCloseForm: Dispatch> }) {
+ const t = useTranslations();
+ const {lang} = useContext(LangContext);
+ const {session} = useContext(SessionContext);
+ const {errorMessage, successMessage} = useContext(AlertContext);
+ const {setServerOnlyBooks} = useContext(BooksSyncContext);
+ const modalRef: RefObject = useRef(null);
+
+ const token: string = session?.accessToken ?? '';
+
+ const [isParsing, setIsParsing] = useState(false);
+ const [isImporting, setIsImporting] = useState(false);
+ const [importId, setImportId] = useState('');
+ const [chapters, setChapters] = useState([]);
+ const [title, setTitle] = useState('');
+ const [subTitle, setSubTitle] = useState('');
+ const [summary, setSummary] = useState('');
+ const [selectedBookType, setSelectedBookType] = useState('short');
+ const [selectedVersion, setSelectedVersion] = useState('2');
+
+ const hasParsedFile: boolean = importId.length > 0 && chapters.length > 0;
+ const selectedCount: number = chapters.filter((chapter: ImportChapterSelection): boolean => chapter.selected).length;
+ const canImport: boolean = !isImporting && hasParsedFile && selectedCount > 0 && title.trim().length > 0;
+
+ useEffect((): () => void => {
+ document.body.style.overflow = 'hidden';
+ return (): void => {
+ document.body.style.overflow = 'auto';
+ };
+ }, []);
+
+ const handleFileChange = useCallback(async (e: ChangeEvent): Promise => {
+ const file: File | undefined = e.target.files?.[0];
+ if (!file) return;
+
+ if (!file.name.endsWith('.docx')) {
+ errorMessage(t('importBook.error.invalidFormat'));
+ return;
+ }
+
+ setIsParsing(true);
+ setImportId('');
+ setChapters([]);
+
+ try {
+ const response: ParsedDocxResponse = await System.authUploadFileToServer(
+ 'book/import/parse',
+ file,
+ token,
+ lang,
+ );
+
+ setImportId(response.importId);
+ setChapters(
+ response.chapters.map((chapter: { index: number; title: string; wordCount: number }): ImportChapterSelection => ({
+ index: chapter.index,
+ title: chapter.title,
+ wordCount: chapter.wordCount,
+ selected: true,
+ })),
+ );
+ } catch (parseError: unknown) {
+ if (parseError instanceof Error) {
+ errorMessage(parseError.message);
+ } else {
+ errorMessage(t('importBook.error.parseFailed'));
+ }
+ } finally {
+ setIsParsing(false);
+ }
+ }, [token, lang, errorMessage, t]);
+
+ const toggleChapter = useCallback((chapterIndex: number): void => {
+ setChapters((previousChapters: ImportChapterSelection[]): ImportChapterSelection[] =>
+ previousChapters.map((chapter: ImportChapterSelection): ImportChapterSelection =>
+ chapter.index === chapterIndex ? {...chapter, selected: !chapter.selected} : chapter,
+ ),
+ );
+ }, []);
+
+ const toggleAllChapters = useCallback((selectAll: boolean): void => {
+ setChapters((previousChapters: ImportChapterSelection[]): ImportChapterSelection[] =>
+ previousChapters.map((chapter: ImportChapterSelection): ImportChapterSelection => ({
+ ...chapter,
+ selected: selectAll,
+ })),
+ );
+ }, []);
+
+ const handleImport = useCallback(async (): Promise => {
+ if (!title.trim()) {
+ errorMessage(t('importBook.error.titleRequired'));
+ return;
+ }
+ if (!selectedBookType) {
+ errorMessage(t('importBook.error.typeRequired'));
+ return;
+ }
+ if (selectedCount === 0) {
+ errorMessage(t('importBook.error.noChaptersSelected'));
+ return;
+ }
+
+ setIsImporting(true);
+ try {
+ const selectedChapterIndexes: number[] = chapters
+ .filter((chapter: ImportChapterSelection): boolean => chapter.selected)
+ .map((chapter: ImportChapterSelection): number => chapter.index);
+
+ await System.authPostToServer<{ bookId: string }>(
+ 'book/import',
+ {
+ importId,
+ title: title.trim(),
+ subTitle: subTitle.trim(),
+ summary: summary.trim(),
+ type: selectedBookType,
+ version: parseInt(selectedVersion, 10),
+ selectedChapterIndexes,
+ },
+ token,
+ lang,
+ );
+
+ setServerOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => [...prevBooks, {
+ id: importId,
+ type: selectedBookType,
+ title: title.trim(),
+ subTitle: subTitle.trim(),
+ lastUpdate: new Date().getTime() / 1000,
+ chapters: [],
+ characters: [],
+ locations: [],
+ worlds: [],
+ incidents: [],
+ plotPoints: [],
+ issues: [],
+ actSummaries: [],
+ guideLine: null,
+ aiGuideLine: null,
+ bookTools: null,
+ seriesId: null,
+ spells: [],
+ spellTags: []
+ }]);
+
+ successMessage(t('importBook.success'));
+ setCloseForm(false);
+ } catch (importError: unknown) {
+ if (importError instanceof Error) {
+ errorMessage(importError.message);
+ } else {
+ errorMessage(t('importBook.error.importFailed'));
+ }
+ } finally {
+ setIsImporting(false);
+ }
+ }, [title, subTitle, summary, selectedBookType, selectedVersion, importId, chapters, selectedCount, token, lang, errorMessage, successMessage, t, setCloseForm, setServerOnlyBooks]);
+
+ return (
+
+
+
+
+
+ {t("importBook.header.title")}
+
+ setCloseForm(false)}
+ >
+
+
+
+
+
+
+
+ {isParsing ? (
+
+ ) : (
+
+ )}
+
+ {isParsing ? t('importBook.parsing') : t('importBook.pickFile')}
+
+
+
+
+ {hasParsedFile && (
+ <>
+
): void => setSelectedBookType(e.target.value)}
+ data={bookTypes.map((types: SelectBoxProps): SelectBoxProps => ({
+ value: types.value,
+ label: t(types.label)
+ }))}
+ defaultValue={selectedBookType}
+ />
+ }/>
+
+ ): void => setTitle(e.target.value)}
+ placeholder={t("importBook.fields.title.placeholder")}
+ />
+ }/>
+
+ ): void => setSubTitle(e.target.value)}
+ placeholder={t("importBook.fields.subTitle.placeholder")}
+ />
+ }/>
+
+ ): void => setSummary(e.target.value)}
+ placeholder={t("importBook.fields.summary.placeholder")}
+ />
+ }/>
+
+ ): void => setSelectedVersion(e.target.value)}
+ data={chapterVersions.map((version: SelectBoxProps): SelectBoxProps => ({
+ value: version.value,
+ label: t(version.label)
+ }))}
+ defaultValue={selectedVersion}
+ />
+ }/>
+
+
+
+
+ {t('importBook.chapters.title')}
+
+
+ {t('importBook.chaptersDetected', {count: chapters.length})}
+
+
+
+
toggleAllChapters(selectedCount < chapters.length)}
+ className="text-sm text-primary hover:text-primary/80 mb-3 transition-colors"
+ >
+ {selectedCount === chapters.length
+ ? t('importBook.chapters.deselectAll')
+ : t('importBook.chapters.selectAll')}
+
+
+
+ {chapters.map((chapter: ImportChapterSelection) => (
+
toggleChapter(chapter.index)}
+ className="flex items-center gap-3 w-full py-2 px-3 rounded-lg hover:bg-secondary/30 transition-colors text-left"
+ >
+
+
+
+ {chapter.title}
+
+
+ {t('importBook.chapters.words', {count: chapter.wordCount})}
+
+
+
+ ))}
+
+
+ >
+ )}
+
+
+
+ {hasParsedFile && (
+
+
+
+ setCloseForm(false)}/>
+
+
+
+ )}
+
+
+ );
+}
diff --git a/components/book/settings/ExportSetting.tsx b/components/book/settings/ExportSetting.tsx
new file mode 100644
index 0000000..2439cde
--- /dev/null
+++ b/components/book/settings/ExportSetting.tsx
@@ -0,0 +1,230 @@
+'use client'
+import React, {useCallback, useContext, useEffect, useState} from 'react';
+import {useTranslations} from 'next-intl';
+import {BookContext} from '@/context/BookContext';
+import {AlertContext} from '@/context/AlertContext';
+import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
+import {faDownload, faSpinner} from '@fortawesome/free-solid-svg-icons';
+import {
+ ChapterExportInfo,
+ ChapterExportSelection,
+ chapterVersions,
+ ExportFormat
+} from '@/lib/models/Chapter';
+
+const exportFormats: {value: ExportFormat; label: string}[] = [
+ {value: 'epub', label: 'EPUB'},
+ {value: 'pdf', label: 'PDF'},
+ {value: 'docx', label: 'DOCX'},
+];
+
+export default function ExportSetting(): React.JSX.Element {
+ const t = useTranslations();
+ const {book} = useContext(BookContext);
+ const {errorMessage, successMessage} = useContext(AlertContext);
+
+ const [format, setFormat] = useState('epub');
+ const [chapters, setChapters] = useState([]);
+ const [selections, setSelections] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isExporting, setIsExporting] = useState(false);
+
+ const loadChapters = useCallback(async (): Promise => {
+ if (!book) return;
+ setIsLoading(true);
+ try {
+ const chaptersInfo: ChapterExportInfo[] = await window.electron.invoke(
+ 'db:book:export:info',
+ {bookId: book.bookId}
+ );
+ setChapters(chaptersInfo);
+ const initialSelections: ChapterExportSelection[] = chaptersInfo.map(
+ (ch: ChapterExportInfo): ChapterExportSelection => ({
+ chapterId: ch.chapterId,
+ version: ch.availableVersions[ch.availableVersions.length - 1],
+ selected: true
+ })
+ );
+ setSelections(initialSelections);
+ } catch {
+ errorMessage(t('exportOption.error'));
+ } finally {
+ setIsLoading(false);
+ }
+ }, [book]);
+
+ useEffect((): void => {
+ loadChapters();
+ }, [loadChapters]);
+
+ function toggleChapter(chapterId: string): void {
+ setSelections((prev: ChapterExportSelection[]): ChapterExportSelection[] =>
+ prev.map((s: ChapterExportSelection): ChapterExportSelection =>
+ s.chapterId === chapterId ? {...s, selected: !s.selected} : s
+ )
+ );
+ }
+
+ function toggleAll(selectAll: boolean): void {
+ setSelections((prev: ChapterExportSelection[]): ChapterExportSelection[] =>
+ prev.map((s: ChapterExportSelection): ChapterExportSelection => ({...s, selected: selectAll}))
+ );
+ }
+
+ function updateVersion(chapterId: string, version: number): void {
+ setSelections((prev: ChapterExportSelection[]): ChapterExportSelection[] =>
+ prev.map((s: ChapterExportSelection): ChapterExportSelection =>
+ s.chapterId === chapterId ? {...s, version} : s
+ )
+ );
+ }
+
+ function getVersionLabel(version: number): string {
+ const found = chapterVersions.find((v) => v.value === String(version));
+ return found ? t(found.label) : `v${version}`;
+ }
+
+ async function handleExport(): Promise {
+ if (!book) return;
+ setIsExporting(true);
+ try {
+ const selectedChapters = selections
+ .filter((s: ChapterExportSelection): boolean => s.selected)
+ .map((s: ChapterExportSelection) => ({chapterId: s.chapterId, version: s.version}));
+
+ const result: boolean = await window.electron.invoke('db:book:export', {
+ bookId: book.bookId,
+ format,
+ selections: selectedChapters.length === chapters.length ? null : selectedChapters
+ });
+
+ if (result) {
+ successMessage(t('exportOption.success'));
+ }
+ } catch {
+ errorMessage(t('exportOption.error'));
+ } finally {
+ setIsExporting(false);
+ }
+ }
+
+ const allSelected: boolean = selections.every((s: ChapterExportSelection): boolean => s.selected);
+ const hasSelection: boolean = selections.some((s: ChapterExportSelection): boolean => s.selected);
+
+ return (
+
+
+
{t('exportOption.format')}
+
+ {exportFormats.map(({value, label}) => (
+ setFormat(value)}
+ className={`px-4 py-2 rounded-lg font-medium transition-all duration-200 ${
+ format === value
+ ? 'bg-primary text-white shadow-md'
+ : 'bg-secondary/30 text-text-secondary hover:bg-secondary/50'
+ }`}
+ >
+ {label}
+
+ ))}
+
+
+
+
+
+
{t('exportOption.chapters')}
+ {chapters.length > 0 && (
+ toggleAll(!allSelected)}
+ className="text-sm text-primary hover:text-primary/80 transition-colors"
+ >
+ {allSelected ? t('exportOption.deselectAll') : t('exportOption.selectAll')}
+
+ )}
+
+
+ {isLoading ? (
+
+
+ {t('exportOption.loadingChapters')}
+
+ ) : chapters.length === 0 ? (
+
{t('exportOption.noChapters')}
+ ) : (
+
+ {chapters.map((chapter: ChapterExportInfo) => {
+ const selection: ChapterExportSelection | undefined = selections.find(
+ (s: ChapterExportSelection): boolean => s.chapterId === chapter.chapterId
+ );
+ if (!selection) return null;
+
+ return (
+
+
+ toggleChapter(chapter.chapterId)}
+ className="w-4 h-4 rounded accent-primary cursor-pointer"
+ />
+
+ {chapter.title}
+
+
+
+ {chapter.availableVersions.length > 1 && selection.selected && (
+ ): void =>
+ updateVersion(chapter.chapterId, parseInt(e.target.value, 10))
+ }
+ className="text-xs bg-secondary/50 text-text-primary rounded-md px-2 py-1 border border-secondary/30 focus:outline-none focus:border-primary"
+ >
+ {chapter.availableVersions.map((v: number) => (
+
+ {getVersionLabel(v)}
+
+ ))}
+
+ )}
+
+ );
+ })}
+
+ )}
+
+
+
+ {isExporting ? (
+ <>
+
+ {t('exportOption.exporting')}
+ >
+ ) : (
+ <>
+
+ {t('exportOption.export')}
+ >
+ )}
+
+
+ );
+}
diff --git a/electron/database/models/Export.ts b/electron/database/models/Export.ts
new file mode 100644
index 0000000..50d5df1
--- /dev/null
+++ b/electron/database/models/Export.ts
@@ -0,0 +1,211 @@
+import {AlignmentType, Document, HeadingLevel, Packer, Paragraph, TextRun} from "docx";
+import PDFDocument from "pdfkit";
+import JSZip from "jszip";
+import {mainStyle} from "./EpubStyle.js";
+import Chapter, {ChapterContentData, CompleteChapterContent} from "./Chapter.js";
+import {CompleteBookData} from "./Book.js";
+import System from "../System.js";
+
+export interface ExportResult {
+ buffer: Buffer;
+ fileName: string;
+}
+
+export default class Export {
+ static async transformToDOCX(bookData: CompleteBookData): Promise {
+ const bookTitle: string = bookData.title;
+ const filename: string = `${bookTitle}.docx`;
+
+ const docParagraphs: Paragraph[] = [];
+
+ docParagraphs.push(
+ new Paragraph({
+ children: [
+ new TextRun({text: bookTitle, bold: true, size: 48}),
+ ],
+ alignment: AlignmentType.CENTER,
+ spacing: {after: 400},
+ })
+ );
+
+ if (bookData.subTitle) {
+ docParagraphs.push(
+ new Paragraph({
+ children: [
+ new TextRun({text: bookData.subTitle, italics: true, size: 32}),
+ ],
+ alignment: AlignmentType.CENTER,
+ spacing: {after: 300},
+ })
+ );
+ }
+
+ if (bookData.summary) {
+ docParagraphs.push(
+ new Paragraph({
+ children: [
+ new TextRun({text: bookData.summary, size: 24, italics: true}),
+ ],
+ alignment: AlignmentType.JUSTIFIED,
+ spacing: {after: 400},
+ })
+ );
+ }
+
+ const chapters: ChapterContentData[] = Chapter.getChaptersOrSheet(bookData.chapters);
+
+ for (const chapter of chapters) {
+ if (!chapter.content) continue;
+
+ docParagraphs.push(
+ new Paragraph({
+ text: chapter.title,
+ heading: HeadingLevel.HEADING_1,
+ pageBreakBefore: true,
+ alignment: AlignmentType.CENTER,
+ spacing: {after: 200},
+ })
+ );
+
+ const paragraphs: string[] = chapter.content.split(/\r?\n/);
+
+ for (const paragraph of paragraphs) {
+ if (paragraph.trim() === "") continue;
+ docParagraphs.push(
+ new Paragraph({
+ children: [new TextRun({text: paragraph, size: 24})],
+ alignment: AlignmentType.JUSTIFIED,
+ spacing: {after: 200},
+ })
+ );
+ }
+ }
+
+ const doc: Document = new Document({
+ sections: [{children: docParagraphs}],
+ });
+
+ const buffer: Buffer = await Packer.toBuffer(doc) as Buffer;
+
+ return {buffer, fileName: filename};
+ }
+
+ static async transformToPDF(bookData: CompleteBookData): Promise {
+ const bookTitle: string = bookData.title;
+ const filename: string = `${bookTitle}.pdf`;
+ const chunks: Buffer[] = [];
+ const pdfDoc: PDFKit.PDFDocument = new PDFDocument();
+
+ pdfDoc.on('data', (chunk: Buffer): void => {
+ chunks.push(chunk);
+ });
+
+ pdfDoc.fontSize(20).text(bookTitle, {align: 'center'});
+ pdfDoc.moveDown();
+
+ if (bookData.subTitle && bookData.subTitle.trim() !== '') {
+ pdfDoc.fontSize(16).text(bookData.subTitle, {align: 'center'});
+ pdfDoc.moveDown();
+ }
+
+ if (bookData.summary && bookData.summary.trim() !== '') {
+ pdfDoc.fontSize(12).text(bookData.summary, {align: 'justify'});
+ pdfDoc.moveDown();
+ }
+
+ const chapters: ChapterContentData[] = Chapter.getChaptersOrSheet(bookData.chapters);
+
+ for (const chapter of chapters) {
+ if (!chapter.content) continue;
+ pdfDoc.addPage();
+ pdfDoc.fontSize(16).text(chapter.title, {align: 'center'});
+ pdfDoc.moveDown();
+ pdfDoc.fontSize(12).text(chapter.content, {align: 'justify'});
+ }
+
+ pdfDoc.end();
+
+ await new Promise((resolve: () => void, reject: (reason: Error) => void) => {
+ pdfDoc.on('end', resolve);
+ pdfDoc.on('error', reject);
+ });
+
+ const pdfBuffer: Buffer = Buffer.concat(chunks);
+ return {buffer: pdfBuffer, fileName: filename};
+ }
+
+ static async transformToEpub(bookData: CompleteBookData): Promise {
+ const bookTitle: string = bookData.title;
+ const bookId: string = bookData.bookId;
+ const epub: JSZip = new JSZip();
+
+ epub.file('mimetype', 'application/epub+zip', {compression: 'STORE'});
+ epub.file('META-INF/container.xml', `
+
+
+
+
+ `);
+
+ let contentOpf: string = `
+
+
+ ${bookTitle}${bookData.subTitle ? ' - ' + bookData.subTitle : ''}
+ fr
+ urn:uuid:${bookId}
+ ${bookData.userInfos.firstName} ${bookData.userInfos.lastName}
+ ERitors Scribe
+
+
+ `;
+
+ let spine: string = ``;
+
+ const hasRegularChapters: boolean = bookData.chapters.some(
+ (chapter: CompleteChapterContent): boolean => chapter.order > 0
+ );
+ const chaptersToExport: CompleteChapterContent[] = hasRegularChapters
+ ? bookData.chapters.filter((chapter: CompleteChapterContent): boolean => chapter.order > 0)
+ : bookData.chapters.filter((chapter: CompleteChapterContent): boolean => chapter.order === -1);
+
+ for (const chapter of chaptersToExport) {
+ if (!chapter.content) continue;
+ const chapterIndex: string = `chapter${chapter.order}`;
+ const htmlContent: string = Chapter.tipTapToHtml(JSON.parse(chapter.content) as JSON);
+
+ const xhtmlPage: string = `
+
+
+ ${chapter.title}
+
+
+
+ ${htmlContent}
+
+ `;
+
+ epub.file(`OEBPS/${chapterIndex}.xhtml`, xhtmlPage);
+ contentOpf += ` `;
+ spine += ` `;
+ }
+
+ spine += ` `;
+
+ contentOpf += `
+ `;
+ contentOpf += spine;
+ contentOpf += ` `;
+
+ epub.file('OEBPS/content.opf', contentOpf);
+ epub.file('OEBPS/styles.css', mainStyle);
+
+ if (bookData.coverImage) {
+ const imageBuffer: Buffer = Buffer.from(bookData.coverImage, 'base64');
+ epub.file('OEBPS/cover.jpg', imageBuffer);
+ }
+
+ const epubBuffer: Buffer = await epub.generateAsync({type: 'nodebuffer'}) as Buffer;
+
+ return {buffer: epubBuffer, fileName: `${bookTitle}.epub`};
+ }
+}
diff --git a/lib/configs.ts b/lib/configs.ts
new file mode 100644
index 0000000..8251ffc
--- /dev/null
+++ b/lib/configs.ts
@@ -0,0 +1,19 @@
+import packageJson from '../package.json';
+
+export interface Configs {
+ apiUrl: string;
+ baseUrl: string;
+ appName: string;
+ appDescription: string;
+ appVersion: string;
+}
+
+const isProduction: boolean = true;
+
+export const configs: Configs = {
+ apiUrl: isProduction ? 'https://api.eritors.com/' : 'http://localhost:3001/',
+ baseUrl: isProduction ? 'https://scribe.eritors.com/' : 'http://localhost:3000/',
+ appName: 'ERitors Scribe',
+ appDescription: 'ERitors Scribe est une application de prise de notes et d\'écriture collaborative.',
+ appVersion: packageJson.version,
+};
\ No newline at end of file
diff --git a/lib/models/Import.ts b/lib/models/Import.ts
new file mode 100644
index 0000000..86913d7
--- /dev/null
+++ b/lib/models/Import.ts
@@ -0,0 +1,17 @@
+export interface ParsedChapterPreview {
+ index: number;
+ title: string;
+ wordCount: number;
+}
+
+export interface ParsedDocxResponse {
+ importId: string;
+ chapters: ParsedChapterPreview[];
+}
+
+export interface ImportChapterSelection {
+ index: number;
+ title: string;
+ wordCount: number;
+ selected: boolean;
+}