import { useState, useEffect } from 'react' import { adminApi } from './api' import AdminDashboard from './AdminDashboard' import CohortsView from './CohortsView' import StudentsView from './StudentsView' import InstructorsView from './InstructorsView' import AuditView from './AuditView' import AccountView from './AccountView' import LearningView from './LearningView' import AssessmentsView from './AssessmentsView' import ClassroomView from './ClassroomView' import ProgramsView from './ProgramsView' import NotificationsView from './NotificationsView' import CurriculumView from './CurriculumView' import styles from './Admin.module.css' // Instructor/Admin portal — clearly separate from the student experience (/admin route, // separate rn_instructor session cookie, separate login). A student session cannot reach any // of these views or their underlying /api/admin/* routes. Per-person accounts replace Phase // 2B's single shared ADMIN_PASSWORD — see docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md. // Learning/Assessments (Phase 3B) are visible to every instructor role — cohort scoping for a // plain 'instructor' happens server-side per route, not by hiding the whole tab. const NAV = [ { key: 'dashboard', label: 'Dashboard' }, { key: 'cohorts', label: 'Cohorts' }, { key: 'students', label: 'Students' }, { key: 'programs', label: 'Programs' }, { key: 'curriculum', label: 'Curriculum' }, { key: 'classroom', label: 'Classroom' }, { key: 'learning', label: 'Learning' }, { key: 'assessments', label: 'Assessments' }, { key: 'notifications', label: 'Notifications', roles: ['owner', 'admin'] }, { key: 'instructors', label: 'Instructors', roles: ['owner'] }, { key: 'audit', label: 'Audit', roles: ['owner', 'admin'] }, ] export default function AdminPortal() { const [instructor, setInstructor] = useState(null) // null = checking, false = need login, object = signed in const [view, setView] = useState('dashboard') useEffect(() => { adminApi.session().then((r) => setInstructor(r.instructor)).catch(() => setInstructor(false)) }, []) if (instructor === null) return null if (!instructor) return const visibleNav = NAV.filter((item) => !item.roles || item.roles.includes(instructor.role)) return (
⚙ RootNode Admin
{view === 'dashboard' && } {view === 'cohorts' && } {view === 'students' && } {view === 'programs' && } {view === 'curriculum' && } {view === 'classroom' && } {view === 'learning' && } {view === 'assessments' && } {view === 'notifications' && ['owner', 'admin'].includes(instructor.role) && } {view === 'instructors' && instructor.role === 'owner' && } {view === 'audit' && ['owner', 'admin'].includes(instructor.role) && } {view === 'account' && }
) } function InstructorLogin({ onLogin }) { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState(null) const [busy, setBusy] = useState(false) const submit = async (e) => { e.preventDefault() setBusy(true); setError(null) try { const { instructor } = await adminApi.login(email, password) onLogin(instructor) } catch (e) { setError(e.message || 'Sign-in failed.') } finally { setBusy(false) } } return (
⚙ RootNode Admin

Instructor sign-in. Not connected to student accounts.

{error &&
{error}
} setEmail(e.target.value)} autoFocus autoComplete="username" /> setPassword(e.target.value)} autoComplete="current-password" />
) }