363 lines
10 KiB
React
363 lines
10 KiB
React
import React, { useState, useEffect } from 'react';
|
|
import './App.css';
|
|
import HomeView from './views/HomeView';
|
|
import SearchView from './views/SearchView';
|
|
import ProductView from './views/ProductView';
|
|
import ScannerView from './views/ScannerView';
|
|
import AlertsView from './views/AlertsView';
|
|
import ProfileView from './views/ProfileView';
|
|
import AdminView from './views/AdminView';
|
|
import LoginModal from './components/LoginModal';
|
|
import ForgotPasswordModal from './components/ForgotPasswordModal';
|
|
import ResetPasswordView from './views/ResetPasswordView';
|
|
import SavedNotifications from './components/SavedNotifications';
|
|
import BottomNav from './components/BottomNav';
|
|
import CookieBanner from './components/CookieBanner';
|
|
import HealthConsentModal from './components/HealthConsentModal';
|
|
import PrivacyView from './views/PrivacyView';
|
|
import { getFaro } from './utils/faro';
|
|
import { hasConsentChoice, getLocalConsents, saveConsents, fetchServerConsents } from './utils/consent';
|
|
|
|
function App() {
|
|
const [screen, setScreen] = useState('home');
|
|
const [currentUser, setCurrentUser] = useState(null);
|
|
const [authChecked, setAuthChecked] = useState(false);
|
|
const [showLogin, setShowLogin] = useState(false);
|
|
const [showForgot, setShowForgot] = useState(false);
|
|
const [forgotMode, setForgotMode] = useState('password');
|
|
const [resetToken, setResetToken] = useState(null);
|
|
const [showSaved, setShowSaved] = useState(false);
|
|
const [badgeCount, setBadgeCount] = useState(0);
|
|
const [prescriptionSearch, setPrescriptionSearch] = useState('');
|
|
const [productScreen, setProductScreen] = useState(null);
|
|
const [showCookieBanner, setShowCookieBanner] = useState(!hasConsentChoice());
|
|
const [showHealthConsent, setShowHealthConsent] = useState(false);
|
|
const [pendingTsiScan, setPendingTsiScan] = useState(null);
|
|
const [consents, setConsents] = useState(getLocalConsents);
|
|
const [screenSize, setScreenSize] = useState({
|
|
width: window.innerWidth,
|
|
height: window.innerHeight
|
|
});
|
|
|
|
// Theme: 'auto' | 'light' | 'dark'
|
|
const [theme, setThemeState] = useState(() => {
|
|
return localStorage.getItem('ff-theme') || 'auto';
|
|
});
|
|
|
|
// Device detection
|
|
const isMobile = screenSize.width <= 768;
|
|
const isTablet = screenSize.width > 768 && screenSize.width <= 1024;
|
|
const isDesktop = screenSize.width > 1024;
|
|
|
|
// Apply theme to document
|
|
useEffect(() => {
|
|
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
|
|
function applyTheme() {
|
|
const isDark = theme === 'dark' || (theme === 'auto' && mq.matches);
|
|
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
|
}
|
|
|
|
applyTheme();
|
|
|
|
if (theme === 'auto') {
|
|
mq.addEventListener('change', applyTheme);
|
|
return () => mq.removeEventListener('change', applyTheme);
|
|
}
|
|
}, [theme]);
|
|
|
|
function setTheme(newTheme) {
|
|
setThemeState(newTheme);
|
|
localStorage.setItem('ff-theme', newTheme);
|
|
}
|
|
|
|
useEffect(() => {
|
|
// Set initial screen size
|
|
const handleResize = () => {
|
|
setScreenSize({
|
|
width: window.innerWidth,
|
|
height: window.innerHeight
|
|
});
|
|
};
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const token = params.get('token');
|
|
if (token) {
|
|
setResetToken(token);
|
|
setScreen('reset-password');
|
|
}
|
|
|
|
fetch('/api/auth/check')
|
|
.then(r => r.json())
|
|
.then(data => { if (data.authenticated) setCurrentUser(data.user); })
|
|
.catch((err) => {
|
|
console.warn('[auth/check]', err);
|
|
getFaro()?.pushError(err, { type: 'network', url: '/api/auth/check' });
|
|
})
|
|
.finally(() => setAuthChecked(true));
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
return () => window.removeEventListener('resize', handleResize);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!currentUser) { setBadgeCount(0); return; }
|
|
let cancelled = false;
|
|
fetch('/api/notifications/mine', { credentials: 'include' })
|
|
.then(r => r.ok ? r.json() : null)
|
|
.then(data => {
|
|
if (cancelled || !data) return;
|
|
const count = (data.global?.length || 0) + (data.pharmacy?.length || 0);
|
|
setBadgeCount(count);
|
|
})
|
|
.catch((err) => {
|
|
console.warn('[notifications/mine]', err);
|
|
getFaro()?.pushError(err, { type: 'network', url: '/api/notifications/mine' });
|
|
});
|
|
return () => { cancelled = true; };
|
|
}, [currentUser]);
|
|
|
|
useEffect(() => {
|
|
if (currentUser) {
|
|
fetchServerConsents().then(serverConsents => {
|
|
if (serverConsents) setConsents(serverConsents);
|
|
});
|
|
}
|
|
}, [currentUser]);
|
|
|
|
function refreshBadgeCount() {
|
|
if (!currentUser) return;
|
|
fetch('/api/notifications/mine', { credentials: 'include' })
|
|
.then(r => r.ok ? r.json() : null)
|
|
.then(data => {
|
|
if (!data) return;
|
|
setBadgeCount((data.global?.length || 0) + (data.pharmacy?.length || 0));
|
|
})
|
|
.catch((err) => {
|
|
console.warn('[refreshBadge]', err);
|
|
getFaro()?.pushError(err, { type: 'network', url: '/api/notifications/mine' });
|
|
});
|
|
}
|
|
|
|
function handleLogin(user) {
|
|
setCurrentUser(user);
|
|
setShowLogin(false);
|
|
}
|
|
|
|
async function handleLogout() {
|
|
await fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
|
|
setCurrentUser(null);
|
|
setScreen('home');
|
|
}
|
|
|
|
function handleProfileSaved(updated) {
|
|
setCurrentUser(prev => ({ ...prev, ...updated }));
|
|
}
|
|
|
|
function handleForgotPassword(mode) {
|
|
setForgotMode(mode);
|
|
setShowLogin(false);
|
|
setShowForgot(true);
|
|
}
|
|
|
|
function handleResetComplete() {
|
|
setResetToken(null);
|
|
setShowLogin(true);
|
|
window.history.replaceState({}, '', '/');
|
|
}
|
|
|
|
async function handleCookieConsent(newConsents) {
|
|
const saved = await saveConsents(newConsents);
|
|
setConsents(saved);
|
|
setShowCookieBanner(false);
|
|
}
|
|
|
|
function handleTsiScanRequest(scanFn) {
|
|
if (consents.health_data) {
|
|
scanFn();
|
|
} else {
|
|
setPendingTsiScan(() => scanFn);
|
|
setShowHealthConsent(true);
|
|
}
|
|
}
|
|
|
|
function handleHealthConsentAccept() {
|
|
saveConsents({ ...consents, health_data: true });
|
|
setConsents(prev => ({ ...prev, health_data: true }));
|
|
setShowHealthConsent(false);
|
|
if (pendingTsiScan) {
|
|
pendingTsiScan();
|
|
setPendingTsiScan(null);
|
|
}
|
|
}
|
|
|
|
function handleHealthConsentCancel() {
|
|
setShowHealthConsent(false);
|
|
setPendingTsiScan(null);
|
|
}
|
|
|
|
function handleAdminClick() {
|
|
setScreen('admin');
|
|
}
|
|
|
|
const isLoggedIn = Boolean(currentUser);
|
|
|
|
function handleNavChange(tab) {
|
|
if (tab === 'home') { setScreen('home'); return; }
|
|
if (tab === 'search') { setScreen('search'); return; }
|
|
if (tab === 'scan') { setScreen('scan'); return; }
|
|
if (tab === 'alerts') {
|
|
if (currentUser) setScreen('alerts');
|
|
else setShowLogin(true);
|
|
return;
|
|
}
|
|
if (tab === 'profile') {
|
|
if (currentUser) setScreen('profile');
|
|
else setShowLogin(true);
|
|
return;
|
|
}
|
|
if (tab === 'privacy') { setScreen('privacy'); return; }
|
|
}
|
|
|
|
let activeView;
|
|
let deviceClass = isMobile ? 'mobile-view' : 'desktop-view';
|
|
|
|
switch (screen) {
|
|
case 'profile':
|
|
activeView = (
|
|
<ProfileView
|
|
currentUser={currentUser}
|
|
onProfileSaved={handleProfileSaved}
|
|
onShowSaved={() => setShowSaved(true)}
|
|
onLogout={handleLogout}
|
|
onAdminClick={handleAdminClick}
|
|
theme={theme}
|
|
onThemeChange={setTheme}
|
|
/>
|
|
);
|
|
break;
|
|
case 'alerts':
|
|
activeView = (
|
|
<AlertsView
|
|
onNotificationChange={refreshBadgeCount}
|
|
onNavigateToMedicine={(name) => {
|
|
setPrescriptionSearch(name);
|
|
setScreen('search');
|
|
}}
|
|
/>
|
|
);
|
|
break;
|
|
case 'search':
|
|
activeView = (
|
|
<SearchView
|
|
currentUser={currentUser}
|
|
onLoginRequest={() => setShowLogin(true)}
|
|
initialQuery={prescriptionSearch}
|
|
onNavigateToProduct={(source, id) => {
|
|
setProductScreen({ source, id });
|
|
setScreen('product');
|
|
}}
|
|
/>
|
|
);
|
|
break;
|
|
case 'product':
|
|
activeView = (
|
|
<ProductView
|
|
source={productScreen?.source}
|
|
id={productScreen?.id}
|
|
onBack={() => { setProductScreen(null); setScreen('search'); }}
|
|
currentUser={currentUser}
|
|
onLoginRequest={() => setShowLogin(true)}
|
|
/>
|
|
);
|
|
break;
|
|
case 'scan':
|
|
activeView = (
|
|
<ScannerView
|
|
onClose={() => setScreen('home')}
|
|
onSelectMedicine={(name) => {
|
|
setPrescriptionSearch(name);
|
|
setScreen('search');
|
|
}}
|
|
onTsiScanRequest={handleTsiScanRequest}
|
|
consents={consents}
|
|
/>
|
|
);
|
|
break;
|
|
case 'reset-password':
|
|
activeView = (
|
|
<ResetPasswordView
|
|
token={resetToken}
|
|
onReset={handleResetComplete}
|
|
/>
|
|
);
|
|
break;
|
|
case 'admin':
|
|
activeView = <AdminView />;
|
|
break;
|
|
case 'privacy':
|
|
activeView = <PrivacyView onBack={() => setScreen('home')} />;
|
|
break;
|
|
default:
|
|
activeView = (
|
|
<HomeView
|
|
currentUser={currentUser}
|
|
onLoginRequest={() => setShowLogin(true)}
|
|
onScanClick={() => setScreen('scan')}
|
|
onSearchClick={() => setScreen('search')}
|
|
/>
|
|
);
|
|
break;
|
|
}
|
|
|
|
return (
|
|
<div className={`app ${deviceClass}`}>
|
|
<div className="app-content">
|
|
{activeView}
|
|
</div>
|
|
|
|
<BottomNav
|
|
activeTab={screen}
|
|
onChange={handleNavChange}
|
|
isLoggedIn={Boolean(currentUser)}
|
|
badgeCount={badgeCount}
|
|
/>
|
|
|
|
{showLogin && (
|
|
<LoginModal
|
|
onLogin={handleLogin}
|
|
onClose={() => setShowLogin(false)}
|
|
onForgotPassword={handleForgotPassword}
|
|
/>
|
|
)}
|
|
|
|
{showForgot && (
|
|
<ForgotPasswordModal
|
|
initialMode={forgotMode}
|
|
onClose={() => setShowForgot(false)}
|
|
onBackToLogin={() => {
|
|
setShowForgot(false);
|
|
setShowLogin(true);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{showSaved && currentUser && (
|
|
<SavedNotifications onClose={() => setShowSaved(false)} onNotificationChange={refreshBadgeCount} />
|
|
)}
|
|
|
|
{showCookieBanner && (
|
|
<CookieBanner
|
|
onConsent={handleCookieConsent}
|
|
onPrivacyClick={() => { setShowCookieBanner(false); setScreen('privacy'); }}
|
|
/>
|
|
)}
|
|
{showHealthConsent && (
|
|
<HealthConsentModal onAccept={handleHealthConsentAccept} onCancel={handleHealthConsentCancel} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|