Spaces:
Running
Running
| import { useState, useRef, useEffect } from 'react'; | |
| import { Toaster } from 'react-hot-toast'; | |
| import { ThemeProvider } from './context/ThemeContext'; | |
| import { AuthProvider, useAuth } from './context/AuthContext'; | |
| import { FormProvider, useForm } from './context/FormContext'; | |
| import { fetchTemplateData, hydrateTemplateData } from './data/templateData'; | |
| import LoginPage from './components/auth/LoginPage'; | |
| import Navbar from './components/layout/Navbar'; | |
| import Sidebar from './components/layout/Sidebar'; | |
| import PatientHeader from './components/sections/PatientHeader'; | |
| import SectionRenderer from './components/sections/SectionRenderer'; | |
| import AbbreviationPanel from './components/reference/AbbreviationPanel'; | |
| import SmartPhrasesPanel from './components/reference/SmartPhrasesPanel'; | |
| import AIGeneratorPanel from './components/ai/AIGeneratorPanel'; | |
| import PreviewPanel from './components/preview/PreviewPanel'; | |
| import AddSectionModal from './components/modals/AddSectionModal'; | |
| import './index.css'; | |
| function AppContent({ templateSections }) { | |
| const [activeSection, setActiveSection] = useState('_header'); | |
| const [showPreview, setShowPreview] = useState(false); | |
| const [showGenerator, setShowGenerator] = useState(false); | |
| const [showAddSection, setShowAddSection] = useState(false); | |
| const [sidebarOpen, setSidebarOpen] = useState(() => window.innerWidth > 1024); | |
| const mainRef = useRef(null); | |
| const { addCustomSection, formState } = useForm(); | |
| const isMobile = () => window.innerWidth <= 1024; | |
| const handleSectionClick = (sectionId) => { | |
| setActiveSection(sectionId); | |
| const el = document.getElementById(`section-${sectionId}`); | |
| if (el) { | |
| el.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } | |
| // Auto-close sidebar on mobile after clicking a section | |
| if (isMobile()) { | |
| setSidebarOpen(false); | |
| } | |
| }; | |
| const handleAddSection = (title) => { | |
| addCustomSection(title); | |
| }; | |
| return ( | |
| <> | |
| <Navbar | |
| onPreview={() => setShowPreview(true)} | |
| onGenerate={() => setShowGenerator(true)} | |
| onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} | |
| /> | |
| <div className="app-layout"> | |
| {/* Backdrop overlay for mobile sidebar */} | |
| {sidebarOpen && isMobile() && ( | |
| <div | |
| className="sidebar-backdrop" | |
| onClick={() => setSidebarOpen(false)} | |
| /> | |
| )} | |
| <Sidebar | |
| activeSection={activeSection} | |
| onSectionClick={handleSectionClick} | |
| onAddSection={() => setShowAddSection(true)} | |
| isOpen={sidebarOpen} | |
| /> | |
| <main className="main-content" ref={mainRef}> | |
| <div id="section-_header"> | |
| <PatientHeader /> | |
| </div> | |
| <div id="section-_abbreviations"> | |
| <AbbreviationPanel /> | |
| </div> | |
| <div id="section-_smart_phrases"> | |
| <SmartPhrasesPanel /> | |
| </div> | |
| {templateSections.map(section => ( | |
| <SectionRenderer key={section.id} section={section} /> | |
| ))} | |
| {formState.customSections.map(cs => ( | |
| <div key={cs.id} id={`section-${cs.id}`} className="section"> | |
| <div className="section__header"> | |
| <div className="section__header-icon" style={{ background: 'linear-gradient(135deg, var(--accent-500), var(--accent-700))' }}> | |
| <span style={{ color: 'white', fontWeight: 700, fontSize: 'var(--font-sm)' }}>C</span> | |
| </div> | |
| <h2 className="section__title">{cs.title}</h2> | |
| </div> | |
| <div className="subsection"> | |
| <div className="subsection__title"> | |
| <span className="subsection__title-dot" /> | |
| Custom Content | |
| </div> | |
| <p style={{ color: 'var(--text-secondary)', fontSize: 'var(--font-sm)' }}> | |
| This custom section will be included in AI generation. Add relevant notes below. | |
| </p> | |
| <div style={{ marginTop: 'var(--space-3)' }}> | |
| <textarea | |
| className="text-input__field" | |
| placeholder="Enter custom notes, observations, or selections for this section..." | |
| rows={4} | |
| style={{ resize: 'vertical', width: '100%' }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| <div style={{ height: 80 }} /> | |
| </main> | |
| </div> | |
| <PreviewPanel isOpen={showPreview} onClose={() => setShowPreview(false)} /> | |
| <AIGeneratorPanel isOpen={showGenerator} onClose={() => setShowGenerator(false)} /> | |
| <AddSectionModal | |
| isOpen={showAddSection} | |
| onClose={() => setShowAddSection(false)} | |
| onAdd={handleAddSection} | |
| /> | |
| <Toaster | |
| position="bottom-right" | |
| toastOptions={{ | |
| style: { | |
| background: 'var(--surface-card)', | |
| color: 'var(--text-primary)', | |
| border: '1px solid var(--border-primary)', | |
| fontFamily: 'var(--font-family)', | |
| fontSize: 'var(--font-sm)', | |
| }, | |
| }} | |
| /> | |
| </> | |
| ); | |
| } | |
| function AuthGate() { | |
| const { isAuthenticated, loading, token } = useAuth(); | |
| const [templateLoaded, setTemplateLoaded] = useState(false); | |
| const [templateSections, setTemplateSections] = useState([]); | |
| const [templateError, setTemplateError] = useState(''); | |
| // Fetch template data once authenticated | |
| useEffect(() => { | |
| if (isAuthenticated && token && !templateLoaded) { | |
| fetchTemplateData(token) | |
| .then(data => { | |
| const hydrated = hydrateTemplateData(data); | |
| setTemplateSections(hydrated.TEMPLATE_SECTIONS); | |
| setTemplateLoaded(true); | |
| }) | |
| .catch(err => { | |
| setTemplateError(err.message); | |
| }); | |
| } | |
| }, [isAuthenticated, token, templateLoaded]); | |
| // Checking stored token | |
| if (loading) { | |
| return ( | |
| <div className="login-page"> | |
| <div className="loading-container"> | |
| <div className="loading-spinner" /> | |
| <p className="loading-text">Verifying session...</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Not authenticated | |
| if (!isAuthenticated) { | |
| return <LoginPage />; | |
| } | |
| // Authenticated but template not yet loaded | |
| if (!templateLoaded) { | |
| return ( | |
| <div className="login-page"> | |
| <div className="loading-container"> | |
| <div className="loading-spinner" /> | |
| <p className="loading-text"> | |
| {templateError || 'Loading clinical template...'} | |
| </p> | |
| {templateError && ( | |
| <button | |
| className="navbar__btn navbar__btn--primary" | |
| onClick={() => { setTemplateError(''); setTemplateLoaded(false); }} | |
| style={{ marginTop: 'var(--space-4)' }} | |
| > | |
| Retry | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Fully ready | |
| return ( | |
| <FormProvider> | |
| <AppContent templateSections={templateSections} /> | |
| </FormProvider> | |
| ); | |
| } | |
| export default function App() { | |
| return ( | |
| <ThemeProvider> | |
| <AuthProvider> | |
| <AuthGate /> | |
| </AuthProvider> | |
| </ThemeProvider> | |
| ); | |
| } | |