ot / frontend /src /App.jsx
jashdoshi77's picture
OT NoteBuilder - Production deployment
ba95018
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>
);
}