Remove SyncService and introduce context-based offline mode and state management

- Delete `SyncService` and its associated bidirectional synchronization logic.
- Add multiple context providers (`OfflineProvider`, `AlertProvider`, `LangContext`, `UserContext`, `SessionContext`, `WorldContext`, `SettingBookContext`) for contextual state management.
- Implement `SecureStorage` for OS-level secure data encryption and replace dependency on `SyncService` synchronization.
- Update localization files (`en.json`, `fr.json`) with offline mode and error-related strings.
This commit is contained in:
natreex
2025-11-19 22:01:24 -05:00
parent f85c2d2269
commit 9e51cc93a8
20 changed files with 961 additions and 484 deletions

80
context/AlertProvider.tsx Normal file
View File

@@ -0,0 +1,80 @@
'use client';
import type {Context, Dispatch, JSX, ReactNode, SetStateAction} from 'react';
import {createContext, useCallback, useState} from 'react';
import AlertStack from '@/components/AlertStack';
export type AlertType = 'success' | 'error' | 'info' | 'warning';
export interface Alert {
id: string;
type: AlertType;
message: string;
}
export interface AlertContextProps {
successMessage: (message: string) => void;
errorMessage: (message: string) => void;
infoMessage: (message: string) => void;
warningMessage: (message: string) => void;
}
interface AlertProviderProps {
children: ReactNode;
}
export const AlertContext: Context<AlertContextProps> = createContext<AlertContextProps>({
successMessage: (_message: string): void => {
},
errorMessage: (_message: string): void => {
},
infoMessage: (_message: string): void => {
},
warningMessage: (_message: string): void => {
},
});
export function AlertProvider({children}: AlertProviderProps): JSX.Element {
const [alerts, setAlerts]: [Alert[], Dispatch<SetStateAction<Alert[]>>] = useState<Alert[]>([]);
const addAlert: (type: AlertType, message: string) => void = useCallback((type: AlertType, message: string): void => {
const id: string = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
const newAlert: Alert = {id, type, message};
setAlerts((prev: Alert[]): Alert[] => [...prev, newAlert]);
}, []);
const removeAlert: (id: string) => void = useCallback((id: string): void => {
setAlerts((prev: Alert[]): Alert[] => prev.filter((alert: Alert): boolean => alert.id !== id));
}, []);
const successMessage: (message: string) => void = useCallback((message: string): void => {
addAlert('success', message);
}, [addAlert]);
const errorMessage: (message: string) => void = useCallback((message: string): void => {
addAlert('error', message);
}, [addAlert]);
const infoMessage: (message: string) => void = useCallback((message: string): void => {
addAlert('info', message);
}, [addAlert]);
const warningMessage: (message: string) => void = useCallback((message: string): void => {
addAlert('warning', message);
}, [addAlert]);
return (
<AlertContext.Provider
value={{
successMessage,
errorMessage,
infoMessage,
warningMessage,
}}
>
{children}
<AlertStack alerts={alerts} onClose={removeAlert}/>
</AlertContext.Provider>
);
}

13
context/BookContext.ts Executable file
View File

@@ -0,0 +1,13 @@
import {Context, createContext, Dispatch, SetStateAction} from "react";
import {BookProps} from "@/lib/models/Book";
export interface BookContextProps {
book: BookProps | null,
setBook?: Dispatch<SetStateAction<BookProps | null>>
}
export const BookContext: Context<BookContextProps> = createContext<BookContextProps>({
book: null,
setBook: () => {
}
})

14
context/ChapterContext.ts Executable file
View File

@@ -0,0 +1,14 @@
import {ChapterProps} from "@/lib/models/Chapter";
import {createContext, Dispatch, SetStateAction} from "react";
export interface ChapterContextProps {
chapter: ChapterProps | undefined,
setChapter: Dispatch<SetStateAction<ChapterProps | undefined>>
}
export const ChapterContext = createContext<ChapterContextProps>({
chapter: undefined,
setChapter: () => {
}
})

10
context/EditorContext.ts Executable file
View File

@@ -0,0 +1,10 @@
import {createContext} from "react";
import {Editor} from "@tiptap/core";
export interface EditorContextProps {
editor: Editor | null
}
export const EditorContext = createContext<EditorContextProps>({
editor: null
});

14
context/LangContext.ts Normal file
View File

@@ -0,0 +1,14 @@
import {Context, createContext, Dispatch, SetStateAction} from "react";
export type SupportedLocale = 'fr' | 'en';
export interface LangContextProps {
lang: SupportedLocale;
setLang: Dispatch<SetStateAction<SupportedLocale>>;
}
export const LangContext: Context<LangContextProps> = createContext<LangContextProps>({
lang: 'fr',
setLang: (): void => {
}
});

35
context/OfflineContext.ts Normal file
View File

@@ -0,0 +1,35 @@
import { createContext, Dispatch, SetStateAction } from 'react';
export interface OfflineMode {
isManuallyOffline: boolean;
isNetworkOnline: boolean;
isOffline: boolean;
isDatabaseInitialized: boolean;
error: string | null;
}
export interface OfflineContextType {
offlineMode: OfflineMode;
setOfflineMode: Dispatch<SetStateAction<OfflineMode>>;
toggleOfflineMode: () => void;
initializeDatabase: (userId: string, encryptionKey?: string) => Promise<boolean>;
isCurrentlyOffline: () => boolean;
}
export const defaultOfflineMode: OfflineMode = {
isManuallyOffline: false,
isNetworkOnline: typeof navigator !== 'undefined' ? navigator.onLine : true,
isOffline: false,
isDatabaseInitialized: false,
error: null
};
const OfflineContext = createContext<OfflineContextType>({
offlineMode: defaultOfflineMode,
setOfflineMode: () => {},
toggleOfflineMode: () => {},
initializeDatabase: async () => false,
isCurrentlyOffline: () => false
});
export default OfflineContext;

129
context/OfflineProvider.tsx Normal file
View File

@@ -0,0 +1,129 @@
'use client';
import React, { useState, useEffect, useCallback, ReactNode } from 'react';
import OfflineContext, { OfflineMode, defaultOfflineMode } from './OfflineContext';
interface OfflineProviderProps {
children: ReactNode;
}
export default function OfflineProvider({ children }: OfflineProviderProps) {
const [offlineMode, setOfflineMode] = useState<OfflineMode>(defaultOfflineMode);
const initializeDatabase = useCallback(async (userId: string, encryptionKey?: string): Promise<boolean> => {
try {
if (typeof window === 'undefined' || !(window as any).electron) {
console.warn('Not running in Electron, offline mode not available');
return false;
}
let userKey = encryptionKey;
if (!userKey) {
const storedKey = await (window as any).electron.getUserEncryptionKey(userId);
if (storedKey) {
userKey = storedKey;
} else {
const keyResult = await (window as any).electron.generateEncryptionKey(userId);
if (!keyResult.success) {
throw new Error(keyResult.error || 'Failed to generate encryption key');
}
userKey = keyResult.key;
await (window as any).electron.setUserEncryptionKey(userId, userKey);
}
}
const result = await (window as any).electron.dbInitialize(userId, userKey);
if (!result.success) {
throw new Error(result.error || 'Failed to initialize database');
}
setOfflineMode(prev => ({
...prev,
isDatabaseInitialized: true,
error: null
}));
console.log('Database initialized successfully for user:', userId);
return true;
} catch (error) {
console.error('Failed to initialize database:', error);
setOfflineMode(prev => ({
...prev,
isDatabaseInitialized: false,
error: error instanceof Error ? error.message : 'Failed to initialize database'
}));
return false;
}
}, []);
const toggleOfflineMode = useCallback(() => {
setOfflineMode(prev => {
const newManuallyOffline = !prev.isManuallyOffline;
const newIsOffline = newManuallyOffline || !prev.isNetworkOnline;
console.log('Toggle offline mode:', {
wasManuallyOffline: prev.isManuallyOffline,
nowManuallyOffline: newManuallyOffline,
wasOffline: prev.isOffline,
nowOffline: newIsOffline,
networkOnline: prev.isNetworkOnline
});
return {
...prev,
isManuallyOffline: newManuallyOffline,
isOffline: newIsOffline
};
});
}, []);
const isCurrentlyOffline = useCallback((): boolean => {
return offlineMode.isOffline;
}, [offlineMode.isOffline]);
useEffect(() => {
const handleOnline = () => {
setOfflineMode(prev => {
const newIsOffline = prev.isManuallyOffline;
return {
...prev,
isNetworkOnline: true,
isOffline: newIsOffline
};
});
};
const handleOffline = () => {
setOfflineMode(prev => {
return {
...prev,
isNetworkOnline: false,
isOffline: true
};
});
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const value = {
offlineMode,
setOfflineMode,
toggleOfflineMode,
initializeDatabase,
isCurrentlyOffline
};
return (
<OfflineContext.Provider value={value}>
{children}
</OfflineContext.Provider>
);
}

17
context/SessionContext.ts Executable file
View File

@@ -0,0 +1,17 @@
import {SessionProps} from "@/lib/models/Session";
import {Context, createContext, Dispatch, SetStateAction} from "react";
export interface SessionContextProps {
session: SessionProps;
setSession: Dispatch<SetStateAction<SessionProps>>;
}
export const SessionContext: Context<SessionContextProps> = createContext<SessionContextProps>({
session: {
isConnected: false,
accessToken: "",
user: null,
},
setSession: () => {
},
})

12
context/SettingBookContext.ts Executable file
View File

@@ -0,0 +1,12 @@
import {Context, createContext, Dispatch, SetStateAction} from "react";
export interface SettingBookContextProps {
bookSettingId: string;
setBookSettingId: Dispatch<SetStateAction<string>>
}
export const SettingBookContext: Context<SettingBookContextProps> = createContext<SettingBookContextProps>({
bookSettingId: '',
setBookSettingId: (): void => {
}
})

25
context/UserContext.ts Executable file
View File

@@ -0,0 +1,25 @@
import {UserProps} from "@/lib/models/User";
import {Context, createContext} from "react";
export interface UserContextProps {
user: UserProps
}
export const UserContext: Context<UserContextProps> = createContext<UserContextProps>({
user: {
id: '',
name: '',
lastName: '',
username: '',
writingLang: 0,
writingLevel: 0,
ritePoints: 0,
groupId: 0,
aiUsage: 0,
apiKeys: {
openai: false,
anthropic: false,
gemini: false,
}
}
})

10
context/WorldContext.ts Executable file
View File

@@ -0,0 +1,10 @@
import {WorldProps} from "@/lib/models/World";
import {createContext, Dispatch, SetStateAction} from "react";
export interface WorldContextProps {
worlds: WorldProps[];
setWorlds: Dispatch<SetStateAction<WorldProps[]>>;
selectedWorldIndex: number;
}
export const WorldContext = createContext<WorldContextProps>({} as WorldContextProps);