Files
FarmaFinder/apps/frontend/src/App.jsx
T
Antoni Nuñez Romeu e9a1d7c53d
Run Tests on Branches / Detect Changes (push) Successful in 12s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Successful in 1m47s
Run Tests on Branches / Frontend Mobile Tests (push) Has been skipped
Add dark mode for PWA frontend
2026-07-09 13:18:25 +02:00

240 lines
6.7 KiB
React

import React, { useState, useEffect } from 'react';
import './App.css';
import HomeView from './views/HomeView';
import SearchView from './views/SearchView';
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 SavedNotifications from './components/SavedNotifications';
import BottomNav from './components/BottomNav';
import { getFaro } from './utils/faro';
function App() {
const [screen, setScreen] = useState('home');
const [currentUser, setCurrentUser] = useState(null);
const [authChecked, setAuthChecked] = useState(false);
const [showLogin, setShowLogin] = useState(false);
const [showSaved, setShowSaved] = useState(false);
const [badgeCount, setBadgeCount] = useState(0);
const [prescriptionSearch, setPrescriptionSearch] = useState('');
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
});
};
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]);
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 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;
}
}
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}
/>
);
break;
case 'scan':
activeView = (
<ScannerView
onClose={() => setScreen('home')}
onSelectMedicine={(name) => {
setPrescriptionSearch(name);
setScreen('search');
}}
/>
);
break;
case 'admin':
activeView = <AdminView />;
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)}
/>
)}
{showSaved && currentUser && (
<SavedNotifications onClose={() => setShowSaved(false)} onNotificationChange={refreshBadgeCount} />
)}
</div>
);
}
export default App;