Integrate offline logic for book creation and enhance synchronization
- Add offline handling to `AddNewBookForm` by updating `BooksSyncContext` with server-only and local-only book management. - Refactor `guideTourDone` to check offline completion states via `localStorage`. - Update and lock dependencies, including `@esbuild` and `@next`, to latest versions. - Clean up unused session state updates in book creation logic.
This commit is contained in:
50
app/page.tsx
50
app/page.tsx
@@ -45,7 +45,7 @@ function ScribeContent() {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const {lang: locale} = useContext(LangContext);
|
const {lang: locale} = useContext(LangContext);
|
||||||
const {errorMessage} = useContext(AlertContext);
|
const {errorMessage} = useContext(AlertContext);
|
||||||
const {initializeDatabase, setOfflineMode, isCurrentlyOffline} = useContext(OfflineContext);
|
const {initializeDatabase, setOfflineMode, isCurrentlyOffline, offlineMode} = useContext(OfflineContext);
|
||||||
const editor: Editor | null = useEditor({
|
const editor: Editor | null = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit,
|
||||||
@@ -201,19 +201,24 @@ function ScribeContent() {
|
|||||||
|
|
||||||
async function getBooks(): Promise<void> {
|
async function getBooks(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
let localBooksResponse: SyncedBook[]
|
let localBooksResponse: SyncedBook[] = [];
|
||||||
|
let serverBooksResponse: SyncedBook[] = [];
|
||||||
|
|
||||||
if (!isCurrentlyOffline()){
|
if (!isCurrentlyOffline()){
|
||||||
|
// Mode online: récupérer les livres du serveur ET de la DB locale
|
||||||
|
if (offlineMode.isDatabaseInitialized) {
|
||||||
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
|
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
|
||||||
|
}
|
||||||
|
serverBooksResponse = await System.authGetQueryToServer<SyncedBook[]>('books/synced', session.accessToken, locale);
|
||||||
} else {
|
} else {
|
||||||
localBooksResponse = [];
|
// Mode offline: récupérer uniquement depuis la DB locale
|
||||||
|
if (offlineMode.isDatabaseInitialized) {
|
||||||
|
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
|
||||||
}
|
}
|
||||||
const serverBooksResponse: SyncedBook[] = await System.authGetQueryToServer<SyncedBook[]>('books/synced', session.accessToken, locale);
|
}
|
||||||
if (serverBooksResponse) {
|
|
||||||
setServerSyncedBooks(serverBooksResponse);
|
setServerSyncedBooks(serverBooksResponse);
|
||||||
}
|
|
||||||
if (localBooksResponse) {
|
|
||||||
setLocalSyncedBooks(localBooksResponse);
|
setLocalSyncedBooks(localBooksResponse);
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(e.message);
|
errorMessage(e.message);
|
||||||
@@ -250,15 +255,14 @@ function ScribeContent() {
|
|||||||
}, [session.isConnected]);
|
}, [session.isConnected]);
|
||||||
|
|
||||||
async function handlePinVerifySuccess(userId: string): Promise<void> {
|
async function handlePinVerifySuccess(userId: string): Promise<void> {
|
||||||
console.log('[OfflinePin] PIN verified successfully for user:', userId);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (window.electron) {
|
if (window.electron) {
|
||||||
const storedToken: string | null = await window.electron.getToken();
|
const storedToken: string | null = await window.electron.getToken();
|
||||||
|
|
||||||
const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId);
|
const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId);
|
||||||
|
|
||||||
if (encryptionKey) {
|
if (encryptionKey) {
|
||||||
await window.electron.dbInitialize(userId, encryptionKey);
|
await window.electron.dbInitialize(userId, encryptionKey);
|
||||||
|
setOfflineMode(prev => ({...prev, isDatabaseInitialized: true}));
|
||||||
|
|
||||||
const localUser:UserProps = await window.electron.invoke('db:user:info');
|
const localUser:UserProps = await window.electron.invoke('db:user:info');
|
||||||
if (localUser && localUser.id) {
|
if (localUser && localUser.id) {
|
||||||
@@ -272,28 +276,20 @@ function ScribeContent() {
|
|||||||
setAmountSpent(localUser.aiUsage || 0);
|
setAmountSpent(localUser.aiUsage || 0);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t("homePage.errors.localDataError"));
|
errorMessage(t("homePage.errors.localDataError"));
|
||||||
if (window.electron) {
|
|
||||||
//window.electron.logout();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t("homePage.errors.encryptionKeyError"));
|
errorMessage(t("homePage.errors.encryptionKeyError"));
|
||||||
if (window.electron) {
|
|
||||||
//window.electron.logout();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[OfflinePin] Error initializing offline mode:', error);
|
console.error('[OfflinePin] Error initializing offline mode:', error);
|
||||||
errorMessage(t("homePage.errors.offlineModeError"));
|
errorMessage(t("homePage.errors.offlineModeError"));
|
||||||
if (window.electron) {
|
|
||||||
//window.electron.logout();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleHomeTour(): Promise<void> {
|
async function handleHomeTour(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
if (!isCurrentlyOffline()) {
|
||||||
const response: boolean = await System.authPostToServer<boolean>('logs/tour', {
|
const response: boolean = await System.authPostToServer<boolean>('logs/tour', {
|
||||||
plateforme: 'desktop',
|
plateforme: 'desktop',
|
||||||
tour: 'home-basic'
|
tour: 'home-basic'
|
||||||
@@ -305,6 +301,16 @@ function ScribeContent() {
|
|||||||
setSession(User.setNewGuideTour(session, 'home-basic'));
|
setSession(User.setNewGuideTour(session, 'home-basic'));
|
||||||
setHomeStepsGuide(false);
|
setHomeStepsGuide(false);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Mode offline: stocker dans localStorage
|
||||||
|
const completedGuides = JSON.parse(localStorage.getItem('completedGuides') || '[]');
|
||||||
|
if (!completedGuides.includes('home-basic')) {
|
||||||
|
completedGuides.push('home-basic');
|
||||||
|
localStorage.setItem('completedGuides', JSON.stringify(completedGuides));
|
||||||
|
}
|
||||||
|
setSession(User.setNewGuideTour(session, 'home-basic'));
|
||||||
|
setHomeStepsGuide(false);
|
||||||
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(e.message);
|
errorMessage(e.message);
|
||||||
@@ -465,6 +471,10 @@ function ScribeContent() {
|
|||||||
try {
|
try {
|
||||||
let response: ChapterProps | null
|
let response: ChapterProps | null
|
||||||
if (isCurrentlyOffline()){
|
if (isCurrentlyOffline()){
|
||||||
|
if (!offlineMode.isDatabaseInitialized) {
|
||||||
|
setCurrentChapter(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
response = await window.electron.invoke('db:chapter:last', currentBook?.bookId)
|
response = await window.electron.invoke('db:chapter:last', currentBook?.bookId)
|
||||||
} else {
|
} else {
|
||||||
response = await System.authGetQueryToServer<ChapterProps | null>(`chapter/last-chapter`, session.accessToken, locale, {bookid: currentBook?.bookId});
|
response = await System.authGetQueryToServer<ChapterProps | null>(`chapter/last-chapter`, session.accessToken, locale, {bookid: currentBook?.bookId});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {ChangeEvent, Dispatch, SetStateAction, useContext, useEffect, useRef, useState} from "react";
|
import {ChangeEvent, Dispatch, RefObject, SetStateAction, useContext, useEffect, useRef, useState} from "react";
|
||||||
import {AlertContext} from "@/context/AlertContext";
|
import {AlertContext} from "@/context/AlertContext";
|
||||||
import System from "@/lib/models/System";
|
import System from "@/lib/models/System";
|
||||||
import {SessionContext} from "@/context/SessionContext";
|
import {SessionContext} from "@/context/SessionContext";
|
||||||
@@ -28,6 +28,8 @@ import {UserProps} from "@/lib/models/User";
|
|||||||
import {useTranslations} from "next-intl";
|
import {useTranslations} from "next-intl";
|
||||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||||
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||||
|
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
||||||
|
import {SyncedBook} from "@/lib/models/SyncedBook";
|
||||||
|
|
||||||
interface MinMax {
|
interface MinMax {
|
||||||
min: number;
|
min: number;
|
||||||
@@ -37,10 +39,11 @@ interface MinMax {
|
|||||||
export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<SetStateAction<boolean>> }) {
|
export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<SetStateAction<boolean>> }) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const {lang} = useContext<LangContextProps>(LangContext);
|
const {lang} = useContext<LangContextProps>(LangContext);
|
||||||
const {session, setSession} = useContext(SessionContext);
|
const {session} = useContext(SessionContext);
|
||||||
const {errorMessage} = useContext(AlertContext);
|
const {errorMessage} = useContext(AlertContext);
|
||||||
|
const {setServerOnlyBooks, setLocalOnlyBooks} = useContext<BooksSyncContextProps>(BooksSyncContext)
|
||||||
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
|
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
|
||||||
const modalRef: React.RefObject<HTMLDivElement | null> = useRef<HTMLDivElement>(null);
|
const modalRef: RefObject<HTMLDivElement | null> = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [title, setTitle] = useState<string>('');
|
const [title, setTitle] = useState<string>('');
|
||||||
const [subtitle, setSubtitle] = useState<string>('');
|
const [subtitle, setSubtitle] = useState<string>('');
|
||||||
@@ -159,14 +162,45 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<
|
|||||||
bookId: bookId,
|
bookId: bookId,
|
||||||
...bookData
|
...bookData
|
||||||
};
|
};
|
||||||
|
if (isCurrentlyOffline()){
|
||||||
setSession({
|
setLocalOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => [...prevBooks, {
|
||||||
...session,
|
id: book.bookId,
|
||||||
user: {
|
type: selectedBookType,
|
||||||
...session.user as UserProps,
|
title: title,
|
||||||
books: [...((session.user as UserProps)?.books ?? []), book]
|
subTitle: subtitle,
|
||||||
|
lastUpdate: new Date().getTime()/1000,
|
||||||
|
chapters: [],
|
||||||
|
characters: [],
|
||||||
|
locations: [],
|
||||||
|
worlds: [],
|
||||||
|
incidents: [],
|
||||||
|
plotPoints: [],
|
||||||
|
issues: [],
|
||||||
|
actSummaries: [],
|
||||||
|
guideLine: null,
|
||||||
|
aiGuideLine: null
|
||||||
|
}]);
|
||||||
}
|
}
|
||||||
});
|
else {
|
||||||
|
setServerOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => [...prevBooks, {
|
||||||
|
id: book.bookId,
|
||||||
|
type: selectedBookType,
|
||||||
|
title: title,
|
||||||
|
subTitle: subtitle,
|
||||||
|
lastUpdate: new Date().getTime()/1000,
|
||||||
|
chapters: [],
|
||||||
|
characters: [],
|
||||||
|
locations: [],
|
||||||
|
worlds: [],
|
||||||
|
incidents: [],
|
||||||
|
plotPoints: [],
|
||||||
|
issues: [],
|
||||||
|
actSummaries: [],
|
||||||
|
guideLine: null,
|
||||||
|
aiGuideLine: null
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
setIsAddingBook(false);
|
setIsAddingBook(false);
|
||||||
setCloseForm(false)
|
setCloseForm(false)
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function BookList() {
|
|||||||
const {setBook} = useContext(BookContext);
|
const {setBook} = useContext(BookContext);
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const {lang} = useContext<LangContextProps>(LangContext)
|
const {lang} = useContext<LangContextProps>(LangContext)
|
||||||
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext)
|
const {isCurrentlyOffline, offlineMode} = useContext<OfflineContextType>(OfflineContext)
|
||||||
const {booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, localOnlyBooks} = useContext<BooksSyncContextProps>(BooksSyncContext)
|
const {booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, localOnlyBooks} = useContext<BooksSyncContextProps>(BooksSyncContext)
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
@@ -87,16 +87,28 @@ export default function BookList() {
|
|||||||
}
|
}
|
||||||
}, [groupedBooks]);
|
}, [groupedBooks]);
|
||||||
|
|
||||||
|
// Charger les livres quand les conditions sont remplies
|
||||||
useEffect((): void => {
|
useEffect((): void => {
|
||||||
getBooks().then()
|
const shouldFetchBooks =
|
||||||
}, [booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, localOnlyBooks]);
|
(session.isConnected || accessToken) &&
|
||||||
|
(!isCurrentlyOffline() || offlineMode.isDatabaseInitialized);
|
||||||
|
|
||||||
useEffect((): void => {
|
if (shouldFetchBooks) {
|
||||||
if (accessToken) getBooks().then();
|
getBooks().then();
|
||||||
}, [accessToken]);
|
}
|
||||||
|
}, [
|
||||||
|
session.isConnected,
|
||||||
|
accessToken,
|
||||||
|
offlineMode.isDatabaseInitialized,
|
||||||
|
booksToSyncFromServer,
|
||||||
|
booksToSyncToServer,
|
||||||
|
serverOnlyBooks,
|
||||||
|
localOnlyBooks
|
||||||
|
]);
|
||||||
|
|
||||||
async function handleFirstBookGuide(): Promise<void> {
|
async function handleFirstBookGuide(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
if (!isCurrentlyOffline()) {
|
||||||
const response: boolean = await System.authPostToServer<boolean>(
|
const response: boolean = await System.authPostToServer<boolean>(
|
||||||
'logs/tour',
|
'logs/tour',
|
||||||
{plateforme: 'web', tour: 'new-first-book'},
|
{plateforme: 'web', tour: 'new-first-book'},
|
||||||
@@ -106,6 +118,16 @@ export default function BookList() {
|
|||||||
setSession(User.setNewGuideTour(session, 'new-first-book'));
|
setSession(User.setNewGuideTour(session, 'new-first-book'));
|
||||||
setBookGuide(false);
|
setBookGuide(false);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Mode offline: stocker dans localStorage
|
||||||
|
const completedGuides = JSON.parse(localStorage.getItem('completedGuides') || '[]');
|
||||||
|
if (!completedGuides.includes('new-first-book')) {
|
||||||
|
completedGuides.push('new-first-book');
|
||||||
|
localStorage.setItem('completedGuides', JSON.stringify(completedGuides));
|
||||||
|
}
|
||||||
|
setSession(User.setNewGuideTour(session, 'new-first-book'));
|
||||||
|
setBookGuide(false);
|
||||||
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(e.message);
|
errorMessage(e.message);
|
||||||
@@ -122,7 +144,9 @@ export default function BookList() {
|
|||||||
if (!isCurrentlyOffline()) {
|
if (!isCurrentlyOffline()) {
|
||||||
const [onlineBooks, localBooks]: [BookListProps[], BookListProps[]] = await Promise.all([
|
const [onlineBooks, localBooks]: [BookListProps[], BookListProps[]] = await Promise.all([
|
||||||
System.authGetQueryToServer<BookListProps[]>('books', accessToken, lang),
|
System.authGetQueryToServer<BookListProps[]>('books', accessToken, lang),
|
||||||
window.electron.invoke<BookListProps[]>('db:book:books')
|
offlineMode.isDatabaseInitialized
|
||||||
|
? window.electron.invoke<BookListProps[]>('db:book:books')
|
||||||
|
: Promise.resolve([])
|
||||||
]);
|
]);
|
||||||
const onlineBookIds: Set<string> = new Set(onlineBooks.map((book: BookListProps): string => book.id));
|
const onlineBookIds: Set<string> = new Set(onlineBooks.map((book: BookListProps): string => book.id));
|
||||||
const uniqueLocalBooks: BookListProps[] = localBooks.filter((book: BookListProps): boolean => !onlineBookIds.has(book.id));
|
const uniqueLocalBooks: BookListProps[] = localBooks.filter((book: BookListProps): boolean => !onlineBookIds.has(book.id));
|
||||||
@@ -131,6 +155,10 @@ export default function BookList() {
|
|||||||
...uniqueLocalBooks.map((book: BookListProps): BookListProps & { itIsLocal: boolean } => ({ ...book, itIsLocal: true }))
|
...uniqueLocalBooks.map((book: BookListProps): BookListProps & { itIsLocal: boolean } => ({ ...book, itIsLocal: true }))
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
|
if (!offlineMode.isDatabaseInitialized) {
|
||||||
|
setIsLoadingBooks(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const localBooks: BookListProps[] = await window.electron.invoke<BookListProps[]>('db:book:books');
|
const localBooks: BookListProps[] = await window.electron.invoke<BookListProps[]>('db:book:books');
|
||||||
bookResponse = localBooks.map((book: BookListProps): BookListProps & { itIsLocal: boolean } => ({ ...book, itIsLocal: true }));
|
bookResponse = localBooks.map((book: BookListProps): BookListProps & { itIsLocal: boolean } => ({ ...book, itIsLocal: true }));
|
||||||
}
|
}
|
||||||
@@ -203,6 +231,10 @@ export default function BookList() {
|
|||||||
try {
|
try {
|
||||||
let bookResponse: BookListProps|null = null;
|
let bookResponse: BookListProps|null = null;
|
||||||
if (isCurrentlyOffline()){
|
if (isCurrentlyOffline()){
|
||||||
|
if (!offlineMode.isDatabaseInitialized) {
|
||||||
|
errorMessage(t("bookList.errorBookDetails"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
bookResponse = await window.electron.invoke('db:book:bookBasicInformation', bookId)
|
bookResponse = await window.electron.invoke('db:book:bookBasicInformation', bookId)
|
||||||
} else {
|
} else {
|
||||||
bookResponse = await System.authGetQueryToServer<BookListProps>(`book/basic-information`, accessToken, lang, {
|
bookResponse = await System.authGetQueryToServer<BookListProps>(`book/basic-information`, accessToken, lang, {
|
||||||
|
|||||||
@@ -71,8 +71,22 @@ export default class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static guideTourDone(guide: GuideTour[], tour: string): boolean {
|
static guideTourDone(guide: GuideTour[], tour: string): boolean {
|
||||||
if (!guide || !tour) return false;
|
if (!tour) return false;
|
||||||
return guide.find((guide: GuideTour): boolean => guide[tour]) === undefined;
|
|
||||||
|
// Vérifier d'abord dans le guide du serveur
|
||||||
|
if (guide && guide.find((guide: GuideTour): boolean => guide[tour]) !== undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier ensuite dans localStorage pour le mode offline
|
||||||
|
if (typeof window !== 'undefined' && window.localStorage) {
|
||||||
|
const completedGuides = JSON.parse(localStorage.getItem('completedGuides') || '[]');
|
||||||
|
if (completedGuides.includes(tour)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static setNewGuideTour(session: SessionProps, tour: string): SessionProps {
|
static setNewGuideTour(session: SessionProps, tour: string): SessionProps {
|
||||||
|
|||||||
1940
package-lock.json
generated
1940
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user