diff --git a/app/login/offline/page.tsx b/app/login/offline/page.tsx new file mode 100644 index 0000000..f9e71fd --- /dev/null +++ b/app/login/offline/page.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { useEffect } from 'react'; +import OfflinePinVerify from '@/components/offline/OfflinePinVerify'; +import { useTranslations } from 'next-intl'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faWifi, faArrowLeft } from '@fortawesome/free-solid-svg-icons'; + +export default function OfflineLoginPage() { + const t = useTranslations(); + + async function handlePinSuccess(userId: string):Promise { + + // Initialize database with user's encryption key + if (window.electron) { + try { + // Get encryption key + const encryptionKey = await window.electron.getUserEncryptionKey(userId); + if (encryptionKey) { + // Initialize database + await window.electron.dbInitialize(userId, encryptionKey); + + // Navigate to main page + window.location.href = '/'; + } + } catch (error) { + console.error('[OfflineLogin] Error initializing database:', error); + } + } + } + + function handleBackToOnline():void { + if (window.electron) { + window.electron.logout(); + } + } + + useEffect(():void => { + // Check if we have offline capability + async function checkOfflineCapability() { + if (window.electron) { + const offlineStatus = await window.electron.offlineModeGet(); + if (!offlineStatus.hasPin) { + window.location.href = '/login/login'; + } + } + } + + checkOfflineCapability().then(); + }, []); + + return ( +
+
+ {/* Offline indicator */} +
+
+ + {t('offline.mode.title')} +
+
+ + {/* Logo */} +
+ ERitors +
+ + {/* PIN Verify Component */} + + + {/* Back to online link */} +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index f8bb33e..996ee61 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -30,7 +30,7 @@ import {NextIntlClientProvider, useTranslations} from "next-intl"; import {LangContext} from "@/context/LangContext"; import {AIUsageContext} from "@/context/AIUsageContext"; import OfflineProvider from "@/context/OfflineProvider"; -import OfflineContext from "@/context/OfflineContext"; +import OfflineContext, {OfflineMode} from "@/context/OfflineContext"; import OfflinePinSetup from "@/components/offline/OfflinePinSetup"; import OfflinePinVerify from "@/components/offline/OfflinePinVerify"; import {SyncedBook, BookSyncCompare, compareBookSyncs} from "@/lib/models/SyncedBook"; @@ -234,7 +234,6 @@ function ScribeContent() { if (!offlineStatus.hasPin) { setTimeout(():void => { - console.log('[Page] Showing PIN setup dialog'); setShowPinSetup(true); }, 2000); } @@ -374,13 +373,12 @@ function ScribeContent() { username: user.username, email: user.email }); - console.log('User synced to local DB'); } catch (syncError) { - console.error('Failed to sync user to local DB:', syncError); + errorMessage(t("homePage.errors.syncError")); } } } catch (error) { - console.error('Failed to initialize database:', error); + errorMessage(t("homePage.errors.syncError")); } } setSession({ @@ -396,7 +394,7 @@ function ScribeContent() { const offlineStatus = await window.electron.offlineModeGet(); if (offlineStatus.hasPin && offlineStatus.lastUserId) { - setOfflineMode(prev => ({...prev, isOffline: true, isNetworkOnline: false})); + setOfflineMode((prev:OfflineMode):OfflineMode => ({...prev, isOffline: true, isNetworkOnline: false})); setShowPinVerify(true); setIsLoading(false); return; @@ -407,7 +405,7 @@ function ScribeContent() { } } } catch (offlineError) { - console.error('[Auth] Error checking offline mode:', offlineError); + errorMessage(t("homePage.errors.offlineError")); } } @@ -429,7 +427,7 @@ function ScribeContent() { return; } } catch (error) { - console.error('[Auth] Error checking offline mode:', error); + errorMessage(t("homePage.errors.authenticationError")); } window.electron.logout(); } @@ -557,10 +555,9 @@ function ScribeContent() { showPinSetup && window.electron && ( setShowPinSetup(false)} - onSuccess={() => { + onClose={():void => setShowPinSetup(false)} + onSuccess={():void => { setShowPinSetup(false); - console.log('[Page] PIN configured successfully'); }} /> ) diff --git a/components/SyncBook.tsx b/components/SyncBook.tsx index 5751c69..664db32 100644 --- a/components/SyncBook.tsx +++ b/components/SyncBook.tsx @@ -9,7 +9,7 @@ import {LangContext} from "@/context/LangContext"; import {CompleteBook} from "@/lib/models/Book"; import {BooksSyncContext, BooksSyncContextProps, SyncType} from "@/context/BooksSyncContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; -import {BookSyncCompare} from "@/lib/models/SyncedBook"; +import {BookSyncCompare, SyncedBook} from "@/lib/models/SyncedBook"; interface SyncBookProps { bookId: string; @@ -24,15 +24,48 @@ export default function SyncBook({bookId, status}: SyncBookProps) { const {isCurrentlyOffline} = useContext(OfflineContext); const [isLoading, setIsLoading] = useState(false); const [currentStatus, setCurrentStatus] = useState(status); - const {booksToSyncToServer, booksToSyncFromServer} = useContext(BooksSyncContext) + const {booksToSyncToServer, booksToSyncFromServer,serverSyncedBooks,localSyncedBooks,setLocalOnlyBooks, setServerOnlyBooks} = useContext(BooksSyncContext) const isOffline: boolean = isCurrentlyOffline(); async function upload(): Promise { - // TODO: Implement upload local-only book to server + if (isOffline) { + return; + } + setIsLoading(true); + try { + const bookToSync: CompleteBook = await window.electron.invoke('db:book:uploadToServer', bookId); + if (!bookToSync) { + errorMessage(t("bookCard.uploadError")); + return; + } + const response: boolean = await System.authPostToServer('book/sync/upload', { + book: bookToSync + }, session.accessToken, lang); + if (!response) { + errorMessage(t("bookCard.uploadError")); + return; + } + setCurrentStatus('synced'); + setLocalOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => { + return prevBooks.filter((book: SyncedBook): boolean => book.id !== bookId) + }); + } catch (e:unknown) { + if (e instanceof Error) { + errorMessage(e.message); + } else { + errorMessage(t("bookCard.uploadError")); + } + } finally { + setIsLoading(false); + } } async function download(): Promise { + if (isOffline) { + return; + } + setIsLoading(true); try { const response: CompleteBook = await System.authGetQueryToServer('book/sync/download', session.accessToken, lang, {bookId}); if (!response) { @@ -45,12 +78,17 @@ export default function SyncBook({bookId, status}: SyncBookProps) { return; } setCurrentStatus('synced'); + setServerOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => { + return prevBooks.filter((book: SyncedBook): boolean => book.id !== bookId) + }); } catch (e:unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("bookCard.downloadError")); } + } finally { + setIsLoading(false); } } @@ -58,6 +96,7 @@ export default function SyncBook({bookId, status}: SyncBookProps) { if (isOffline) { return; } + setIsLoading(true); try { const bookToFetch:BookSyncCompare|undefined = booksToSyncFromServer.find((book:BookSyncCompare):boolean => book.id === bookId); if (!bookToFetch) { @@ -83,6 +122,8 @@ export default function SyncBook({bookId, status}: SyncBookProps) { } else { errorMessage(t("bookCard.syncFromServerError")); } + } finally { + setIsLoading(false); } } @@ -90,6 +131,7 @@ export default function SyncBook({bookId, status}: SyncBookProps) { if (isOffline) { return; } + setIsLoading(true); try { const bookToFetch:BookSyncCompare|undefined = booksToSyncToServer.find((book:BookSyncCompare):boolean => book.id === bookId); if (!bookToFetch) { @@ -101,7 +143,7 @@ export default function SyncBook({bookId, status}: SyncBookProps) { errorMessage(t("bookCard.syncToServerError")); return; } - const response: boolean = await System.authPutToServer('book/sync/client-to-server', { + const response: boolean = await System.authPatchToServer('book/sync/client-to-server', { book: bookToSync }, session.accessToken, lang); if (!response) { @@ -115,6 +157,8 @@ export default function SyncBook({bookId, status}: SyncBookProps) { } else { errorMessage(t("bookCard.syncToServerError")); } + } finally { + setIsLoading(false); } } @@ -130,7 +174,6 @@ export default function SyncBook({bookId, status}: SyncBookProps) { return (
- {/* Fully synced - no action needed */} {currentStatus === 'synced' && ( )} - - {/* Local only - can upload to server */} + {currentStatus === 'local-only' && ( )} - - {/* Server only - can download to local */} {currentStatus === 'server-only' && ( )} - - {/* Needs to sync from server (server has newer version) */} {currentStatus === 'to-sync-from-server' && ( )} - - {/* Needs to sync to server (local has newer version) */} {currentStatus === 'to-sync-to-server' && (