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:
80
context/AlertProvider.tsx
Normal file
80
context/AlertProvider.tsx
Normal 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
13
context/BookContext.ts
Executable 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
14
context/ChapterContext.ts
Executable 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
10
context/EditorContext.ts
Executable 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
14
context/LangContext.ts
Normal 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
35
context/OfflineContext.ts
Normal 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
129
context/OfflineProvider.tsx
Normal 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
17
context/SessionContext.ts
Executable 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
12
context/SettingBookContext.ts
Executable 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
25
context/UserContext.ts
Executable 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
10
context/WorldContext.ts
Executable 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);
|
||||
Reference in New Issue
Block a user