Files
flyer-crawler.projectium.com/src/App.tsx
Torben Sorensen 1480a73ab0
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 58s
more compliance
2026-01-09 20:30:52 -08:00

237 lines
8.7 KiB
TypeScript

// src/App.tsx
import React, { useCallback, useEffect } from 'react';
import { Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import * as pdfjsLib from 'pdfjs-dist';
import { Footer } from './components/Footer';
import { Header } from './components/Header';
import { logger } from './services/logger.client';
import type { Profile, UserProfile } from './types';
import { ProfileManager } from './pages/admin/components/ProfileManager';
import { VoiceAssistant } from './features/voice-assistant/VoiceAssistant';
import { AdminPage } from './pages/admin/AdminPage';
import { AdminRoute } from './components/AdminRoute';
import { CorrectionsPage } from './pages/admin/CorrectionsPage';
import { AdminStatsPage } from './pages/admin/AdminStatsPage';
import { FlyerReviewPage } from './pages/admin/FlyerReviewPage';
import { ResetPasswordPage } from './pages/ResetPasswordPage';
import { VoiceLabPage } from './pages/VoiceLabPage';
import { FlyerCorrectionTool } from './components/FlyerCorrectionTool';
import { QuestionMarkCircleIcon } from './components/icons/QuestionMarkCircleIcon';
import { useAuth } from './hooks/useAuth';
import { useFlyers } from './hooks/useFlyers';
import { useFlyerItems } from './hooks/useFlyerItems';
import { useModal } from './hooks/useModal';
import { useFlyerSelection } from './hooks/useFlyerSelection';
import { useDataExtraction } from './hooks/useDataExtraction';
import { MainLayout } from './layouts/MainLayout';
import config from './config';
import { HomePage } from './pages/HomePage';
import { AppGuard } from './components/AppGuard';
import { useAppInitialization } from './hooks/useAppInitialization';
// pdf.js worker configuration
// This is crucial for allowing pdf.js to process PDFs in a separate thread, preventing the UI from freezing.
// We need to explicitly tell pdf.js where to load its worker script from.
// By importing pdfjs-dist, we can host the worker locally, which is more reliable.
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.mjs',
import.meta.url,
).toString();
// Create a client
const queryClient = new QueryClient();
function App() {
const { userProfile, authStatus, login, logout, updateProfile } = useAuth();
const { flyers } = useFlyers();
const { openModal, closeModal, isModalOpen } = useModal();
// Use custom hook for flyer selection logic (auto-select, URL-based selection)
const { selectedFlyer, handleFlyerSelect, flyerIdFromUrl } = useFlyerSelection({
flyers,
});
// This hook now handles initialization effects (OAuth, version check, theme)
// and returns the theme/unit state needed by other components.
const { isDarkMode, unitSystem } = useAppInitialization();
// Use custom hook for data extraction from correction tool
const { handleDataExtracted } = useDataExtraction({
selectedFlyer,
onFlyerUpdate: handleFlyerSelect,
});
// Debugging: Log renders to identify infinite loops (only in test environment)
useEffect(() => {
if (process.env.NODE_ENV === 'test') {
logger.debug(
{
flyersCount: flyers.length,
selectedFlyerId: selectedFlyer?.flyer_id,
flyerIdFromUrl,
authStatus,
profileId: userProfile?.user.user_id,
},
'[App] Render',
);
}
});
const { flyerItems } = useFlyerItems(selectedFlyer);
// Modal handlers
const handleOpenProfile = useCallback(() => openModal('profile'), [openModal]);
const handleCloseProfile = useCallback(() => closeModal('profile'), [closeModal]);
const handleOpenVoiceAssistant = useCallback(() => openModal('voiceAssistant'), [openModal]);
const handleCloseVoiceAssistant = useCallback(() => closeModal('voiceAssistant'), [closeModal]);
const handleOpenWhatsNew = useCallback(() => openModal('whatsNew'), [openModal]);
const handleOpenCorrectionTool = useCallback(() => openModal('correctionTool'), [openModal]);
const handleCloseCorrectionTool = useCallback(() => closeModal('correctionTool'), [closeModal]);
const handleProfileUpdate = useCallback(
(updatedProfileData: Profile) => {
// When the profile is updated, the API returns a `Profile` object.
// We need to merge it with the existing `user` object to maintain
// the `UserProfile` type in our state.
updateProfile(updatedProfileData);
},
[updateProfile],
);
// This is the login handler that will be passed to the ProfileManager component.
const handleLoginSuccess = useCallback(
async (userProfile: UserProfile, token: string, _rememberMe: boolean) => {
try {
await login(token, userProfile);
// After successful login, fetch user-specific data
// The useData hook will automatically refetch user data when `user` changes.
} catch (e) {
// The `login` function within the `useAuth` hook already handles its own errors
// and notifications, so we just need to log any unexpected failures here.
logger.error({ err: e }, 'An error occurred during the login success handling.');
}
},
[login],
);
// Read the application version injected at build time.
// This will only be available in the production build, not during local development.
const appVersion = config.app.version;
return (
// AppGuard now handles the main page wrapper, theme styles, and "What's New" modal
<AppGuard>
<Header
isDarkMode={isDarkMode}
unitSystem={unitSystem}
userProfile={userProfile}
authStatus={authStatus}
onOpenProfile={handleOpenProfile}
onOpenVoiceAssistant={handleOpenVoiceAssistant}
onSignOut={logout}
/>
<ProfileManager
isOpen={isModalOpen('profile')}
onClose={handleCloseProfile}
authStatus={authStatus}
userProfile={userProfile}
onProfileUpdate={handleProfileUpdate}
onLoginSuccess={handleLoginSuccess}
onSignOut={logout}
/>
{userProfile && (
<VoiceAssistant
isOpen={isModalOpen('voiceAssistant')}
onClose={handleCloseVoiceAssistant}
/>
)}
{selectedFlyer && (
<FlyerCorrectionTool
isOpen={isModalOpen('correctionTool')}
onClose={handleCloseCorrectionTool}
imageUrl={selectedFlyer.image_url}
onDataExtracted={handleDataExtracted}
/>
)}
<Routes>
<Route
element={
<MainLayout
onFlyerSelect={handleFlyerSelect}
selectedFlyerId={selectedFlyer?.flyer_id || null}
onOpenProfile={handleOpenProfile}
/>
}
>
<Route
index
element={
<HomePage
selectedFlyer={selectedFlyer}
flyerItems={flyerItems}
onOpenCorrectionTool={handleOpenCorrectionTool}
/>
}
/>
<Route
path="/flyers/:flyerId"
element={
<HomePage
selectedFlyer={selectedFlyer}
flyerItems={flyerItems}
onOpenCorrectionTool={handleOpenCorrectionTool}
/>
}
/>
</Route>
{/* Admin Routes */}
<Route element={<AdminRoute profile={userProfile} />}>
<Route path="/admin" element={<AdminPage />} />
<Route path="/admin/corrections" element={<CorrectionsPage />} />
<Route path="/admin/stats" element={<AdminStatsPage />} />
<Route path="/admin/flyer-review" element={<FlyerReviewPage />} />
<Route path="/admin/voice-lab" element={<VoiceLabPage />} />
</Route>
<Route path="/reset-password/:token" element={<ResetPasswordPage />} />
{/* Add other top-level routes here if needed */}
</Routes>
{appVersion && (
<div className="fixed bottom-2 left-3 z-50 flex items-center space-x-2">
<a
href={config.app.commitUrl || '#'}
target="_blank"
rel="noopener noreferrer"
title="View commit details on Gitea"
className="text-xs text-gray-400 dark:text-gray-600 bg-gray-100 dark:bg-gray-950 px-2 py-1 rounded hover:text-brand-primary dark:hover:text-brand-primary transition-colors"
>
Version: {appVersion}
</a>
<button onClick={handleOpenWhatsNew} title="Show what's new in this version">
<QuestionMarkCircleIcon className="w-5 h-5 text-gray-400 dark:text-gray-600 hover:text-brand-primary dark:hover:text-brand-primary transition-colors" />
</button>
</div>
)}
<Footer />
</AppGuard>
);
}
const WrappedApp = () => (
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
export default WrappedApp;