Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 41s
269 lines
10 KiB
TypeScript
269 lines
10 KiB
TypeScript
// src/App.tsx
|
|
import React, { useState, useCallback, useEffect } from 'react';
|
|
import { Routes, Route, useParams } 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 { Flyer, 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 { 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 [selectedFlyer, setSelectedFlyer] = useState<Flyer | null>(null);
|
|
const { openModal, closeModal, isModalOpen } = useModal();
|
|
const params = useParams<{ flyerId?: string }>();
|
|
|
|
// This hook now handles initialization effects (OAuth, version check, theme)
|
|
// and returns the theme/unit state needed by other components.
|
|
const { isDarkMode, unitSystem } = useAppInitialization();
|
|
|
|
// Debugging: Log renders to identify infinite loops
|
|
useEffect(() => {
|
|
if (process.env.NODE_ENV === 'test') {
|
|
console.log('[App] Render:', {
|
|
flyersCount: flyers.length,
|
|
selectedFlyerId: selectedFlyer?.flyer_id,
|
|
paramsFlyerId: params?.flyerId, // This was a duplicate, fixed.
|
|
authStatus,
|
|
profileId: userProfile?.user.user_id,
|
|
});
|
|
}
|
|
});
|
|
|
|
const { flyerItems } = useFlyerItems(selectedFlyer);
|
|
|
|
// Define modal handlers with useCallback at the top level to avoid Rules of Hooks violations
|
|
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 handleCloseWhatsNew = useCallback(() => closeModal('whatsNew'), [closeModal]);
|
|
|
|
const handleOpenCorrectionTool = useCallback(() => openModal('correctionTool'), [openModal]);
|
|
const handleCloseCorrectionTool = useCallback(() => closeModal('correctionTool'), [closeModal]);
|
|
|
|
const handleDataExtractedFromCorrection = useCallback(
|
|
(type: 'store_name' | 'dates', value: string) => {
|
|
if (!selectedFlyer) return;
|
|
|
|
// This is a simplified update. A real implementation would involve
|
|
// making another API call to update the flyer record in the database.
|
|
// For now, we just update the local state for immediate visual feedback.
|
|
const updatedFlyer = { ...selectedFlyer };
|
|
if (type === 'store_name') {
|
|
updatedFlyer.store = { ...updatedFlyer.store!, name: value };
|
|
} else if (type === 'dates') {
|
|
// A more robust solution would parse the date string properly.
|
|
}
|
|
setSelectedFlyer(updatedFlyer);
|
|
},
|
|
[selectedFlyer],
|
|
);
|
|
|
|
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],
|
|
);
|
|
|
|
// --- State Synchronization and Error Handling ---
|
|
|
|
// 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.
|
|
// We can remove the explicit fetch here.
|
|
} 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],
|
|
);
|
|
|
|
const handleFlyerSelect = useCallback(async (flyer: Flyer) => {
|
|
setSelectedFlyer(flyer);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!selectedFlyer && flyers.length > 0) {
|
|
if (process.env.NODE_ENV === 'test') console.log('[App] Effect: Auto-selecting first flyer');
|
|
handleFlyerSelect(flyers[0]);
|
|
}
|
|
}, [flyers, selectedFlyer, handleFlyerSelect]);
|
|
|
|
// New effect to handle routing to a specific flyer ID from the URL
|
|
useEffect(() => {
|
|
const flyerIdFromUrl = params.flyerId;
|
|
|
|
if (flyerIdFromUrl && flyers.length > 0) {
|
|
const flyerId = parseInt(flyerIdFromUrl, 10);
|
|
const flyerToSelect = flyers.find((f) => f.flyer_id === flyerId);
|
|
if (flyerToSelect && flyerToSelect.flyer_id !== selectedFlyer?.flyer_id) {
|
|
handleFlyerSelect(flyerToSelect);
|
|
}
|
|
}
|
|
}, [flyers, handleFlyerSelect, selectedFlyer, params.flyerId]);
|
|
|
|
// 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={handleDataExtractedFromCorrection}
|
|
/>
|
|
)}
|
|
|
|
<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;
|