import { useCallback, useEffect, useState } from "react"; import { Navigate, Route, Routes, useNavigate } from "react-router-dom"; import { AboutModal, Lightbox, MedDetailModal, ProfileModal, ShareDialog, SharedSchedule, UserFilterModal, } from "./components"; import { AppHeader } from "./components/AppHeader"; import { AuthPage, AuthProvider, useAuth } from "./components/Auth"; import { AppProvider, UnsavedChangesProvider, useAppContext } from "./context"; import { DashboardPage, MedicationsPage, PlannerPage, SchedulePage, SettingsPage } from "./pages"; // Vite injects this at build time from package.json declare const __APP_VERSION__: string; export const FRONTEND_VERSION = typeof __APP_VERSION__ !== "undefined" ? __APP_VERSION__ : "unknown"; const GITHUB_REPO = "DanielVolz/medassist-ng"; export const GITHUB_URL = `https://github.com/${GITHUB_REPO}`; // ============================================================================= // Main App Wrapper with Auth // ============================================================================= export default function App() { return ( {/* Public share route - accessible without auth */} } /> {/* All other routes go through AppRouter */} } /> ); } function AppRouter() { const { user, authState, loading, authError } = useAuth(); // Show loading while checking auth state if (loading) { return (

💊 MedAssist-ng

Loading...

); } // Show error if we couldn't connect to the server if (authError) { return (

💊 MedAssist-ng

Connection Error
{authError}

Please check if the server is running and try again.

); } // If auth state is null (shouldn't happen after loading, but be safe) if (!authState) { return (

💊 MedAssist-ng

Initializing...

); } // If auth is enabled if (authState.authEnabled) { // Need to register first user if (authState.needsSetup) { return ; } // Not logged in if (!user) { return ; } } // Auth disabled or user is logged in - show main app return ( ); } // ============================================================================= // Main App Content // ============================================================================= function AppContent() { const navigate = useNavigate(); // Get shared state from AppContext const ctx = useAppContext(); const { // Medications meds, loadMeds, // Refill showRefillModal, setShowRefillModal, refillPacks, setRefillPacks, refillLoose, setRefillLoose, usePrescriptionRefill, setUsePrescriptionRefill, refillSaving, refillHistory, refillHistoryExpanded, setRefillHistoryExpanded, showEditStockModal, setShowEditStockModal, editStockFullBlisters, setEditStockFullBlisters, editStockPartialBlisterPills, setEditStockPartialBlisterPills, editStockLoosePills, setEditStockLoosePills, editStockSaving, editStockMedication, openRefillModal, closeRefillModal, openEditStockModal, closeEditStockModal, // Share showShareDialog, sharePeople, shareSelectedPerson, setShareSelectedPerson, shareSelectedDays, setShareSelectedDays, shareGenerating, shareLink, setShareLink, shareCopied, setShareCopied, generateShareLink, copyShareLink, closeShareDialog, resetShareDialogState, // Computed coverage, // Modal state selectedMed, setSelectedMed, showImageLightbox, setShowImageLightbox, scheduleLightboxImage, setScheduleLightboxImage, selectedUser, setSelectedUser, // Modal helpers openMedDetail, closeMedDetail, openImageLightbox, closeImageLightbox, closeScheduleLightbox, closeUserFilter, } = ctx; // Wrapper to pass meds to openShareDialog const _openShareDialog = () => ctx.openShareDialog(); // Local-only state (not shared across components) const [showProfile, setShowProfile] = useState(false); const [showAbout, setShowAbout] = useState(false); const closeProfile = useCallback(() => { if (showProfile) { window.history.back(); } }, [showProfile]); const closeAbout = useCallback(() => { if (showAbout) { window.history.back(); } }, [showAbout]); // Get centralized stockThresholds from context const { stockThresholds } = ctx; // Close modal on Escape key useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape") { // Close modals in order of priority (topmost first) if (scheduleLightboxImage) { closeScheduleLightbox(); } else if (showImageLightbox) { closeImageLightbox(); } else if (showEditStockModal) { closeEditStockModal(); } else if (showRefillModal) { closeRefillModal(); } else if (showShareDialog) { closeShareDialog(); } else if (showAbout) { closeAbout(); } else if (showProfile) { closeProfile(); } else if (selectedUser) { closeUserFilter(); } else if (selectedMed) { closeMedDetail(); } } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); }, [ selectedMed, showImageLightbox, scheduleLightboxImage, selectedUser, showProfile, showAbout, showShareDialog, showRefillModal, showEditStockModal, closeAbout, closeEditStockModal, closeImageLightbox, closeMedDetail, closeProfile, closeRefillModal, closeScheduleLightbox, closeShareDialog, closeUserFilter, ]); // Handle browser back button to close modals (in priority order) useEffect(() => { const handlePopState = () => { // Close modals in order of priority (topmost first) // NOTE: This handler MUST NOT call history.back() or it will cause infinite loops // Only use direct state setters here if (showImageLightbox) { setShowImageLightbox(false); } else if (scheduleLightboxImage) { setScheduleLightboxImage(null); } else if (showEditStockModal) { setShowEditStockModal(false); } else if (showRefillModal) { setShowRefillModal(false); } else if (showShareDialog) { resetShareDialogState(); } else if (showAbout) { setShowAbout(false); } else if (showProfile) { setShowProfile(false); } else if (selectedUser) { setSelectedUser(null); } else if (selectedMed) { setSelectedMed(null); } }; window.addEventListener("popstate", handlePopState); return () => window.removeEventListener("popstate", handlePopState); }, [ selectedMed, showImageLightbox, scheduleLightboxImage, selectedUser, showProfile, showAbout, showShareDialog, showRefillModal, showEditStockModal, resetShareDialogState, setScheduleLightboxImage, setSelectedMed, setSelectedUser, setShowEditStockModal, setShowImageLightbox, setShowRefillModal, ]); // Close tooltips on scroll/touch (for mobile) useEffect(() => { const closeAllTooltips = () => { document.querySelectorAll(".info-tooltip.tooltip-active, .tooltip-trigger.tooltip-active").forEach((el) => { el.classList.remove("tooltip-active"); }); }; const handleTooltipClick = (e: Event) => { const target = e.target as HTMLElement; const tooltipTrigger = target.closest(".info-tooltip, .tooltip-trigger") as HTMLElement | null; if (tooltipTrigger) { // Close other tooltips first closeAllTooltips(); // Toggle this one tooltipTrigger.classList.add("tooltip-active"); // Position tooltip above the icon on mobile if (window.innerWidth <= 640) { const rect = tooltipTrigger.getBoundingClientRect(); // Place tooltip bottom edge just above the icon tooltipTrigger.style.setProperty("--tooltip-bottom", `${window.innerHeight - rect.top + 8}px`); } } else { closeAllTooltips(); } }; const handleTouchMove = () => { closeAllTooltips(); }; document.addEventListener("click", handleTooltipClick, { capture: true }); document.addEventListener("touchmove", handleTouchMove, { passive: true }); document.addEventListener("scroll", handleTouchMove, { passive: true }); return () => { document.removeEventListener("click", handleTooltipClick, { capture: true }); document.removeEventListener("touchmove", handleTouchMove); document.removeEventListener("scroll", handleTouchMove); }; }, []); // Prevent background scroll when modal is open useEffect(() => { const isModalOpen = selectedMed || selectedUser || showProfile || showAbout || showShareDialog; if (isModalOpen) { document.documentElement.classList.add("modal-open"); document.body.classList.add("modal-open"); } else { document.documentElement.classList.remove("modal-open"); document.body.classList.remove("modal-open"); } return () => { document.documentElement.classList.remove("modal-open"); document.body.classList.remove("modal-open"); }; }, [selectedMed, selectedUser, showProfile, showAbout, showShareDialog]); // Update selectedMed when meds change (e.g., after refill) useEffect(() => { if (selectedMed) { const updated = meds.find((m) => m.id === selectedMed.id); if ( updated && (updated.packCount !== selectedMed.packCount || updated.looseTablets !== selectedMed.looseTablets || updated.updatedAt !== selectedMed.updatedAt) ) { setSelectedMed(updated); } } }, [meds, selectedMed, setSelectedMed]); const stockCorrectionMed = selectedMed ?? (showEditStockModal ? editStockMedication : null); const handleSubmitStockCorrection = async (medId: number) => { if (!stockCorrectionMed) return; await ctx.submitStockCorrection(medId, stockCorrectionMed, loadMeds); }; // For MedDetailModal: refill without form update (not editing) const handleSubmitRefill = async (medId: number, usePrescription: boolean = false) => { await ctx.submitRefill(medId, null, () => {}, loadMeds, usePrescription); }; const handleOpenMedicationEdit = () => { if (!selectedMed) return; const medId = selectedMed.id; setShowImageLightbox(false); setShowRefillModal(false); setShowEditStockModal(false); setSelectedMed(null); navigate(`/medications?editMedId=${medId}`); }; const handleOpenEditStockFromDetail = () => { if (!selectedMed) return; openEditStockModal(selectedMed, coverage); }; const openProfile = useCallback(() => { setShowProfile(true); window.history.pushState({ modal: "profile" }, ""); }, []); const openAbout = useCallback(() => { setShowAbout(true); window.history.pushState({ modal: "about" }, ""); }, []); return (
{/* Profile Modal */} {/* About Modal */} } /> } /> } /> } /> } /> } /> {/* Catch-all: redirect unknown routes to dashboard */} } /> {/* Medication Detail Modal */} {/* User Medications Modal */} { setSelectedUser(null); // Replace the userFilter history entry so it doesn't remain on the stack window.history.replaceState(null, ""); }} onOpenMedDetail={openMedDetail} /> {/* Share Dialog Modal */} {/* Schedule Lightbox - for clicking medication images in schedule */} {scheduleLightboxImage && ( )}
); }