feat(student): live student experience — real login, classroom, profile
- LoginScreen (credentialed, now the default entry point) with links to legacy pilot entry and forgot-password; SetPasswordScreen (shared activate/reset UI, driven by ?activate=/?reset= query params — no router library, matching this app's existing state-machine-per-shell convention); ForgotPasswordScreen (anti-enumeration, dev-only reset-token surface). - ClassroomTab: LIVE NOW banner, join, heartbeat only while joined and the tab is mounted, guidance-only current-activity link, own Lab status/launch (reuses the My Learning tab's exact API), announcements, Need Help form. No chat, no video, no shell access. - ProfileTab: identity, cohort, progress, achievements, active-session count, change-password (gated on has_credentials). - PlatformShell rewritten as an explicit view state machine; StudentDashboard gained the new tabs; api.js/Platform.module.css extended to match.
This commit is contained in:
@@ -1,17 +1,28 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { api, getCachedDisplay } from './api'
|
||||
import LoginScreen from './LoginScreen'
|
||||
import EntryScreen from './EntryScreen'
|
||||
import SetPasswordScreen from './SetPasswordScreen'
|
||||
import ForgotPasswordScreen from './ForgotPasswordScreen'
|
||||
import StudentDashboard from './StudentDashboard'
|
||||
|
||||
// Top-level Platform Shell: entry screen if no valid session, dashboard otherwise. Session
|
||||
// validity is decided by the server (GET /api/session/whoami against the rn_session cookie),
|
||||
// not by anything read from localStorage/sessionStorage — those only cache the display name
|
||||
// for a snappier first paint, per api.js.
|
||||
// Top-level Platform Shell. Phase 5A: the credentialed LoginScreen is now the default entry
|
||||
// point (was EntryScreen's pilot name-picker); an activation/reset link
|
||||
// (?activate=<token> or ?reset=<token>) short-circuits straight to SetPasswordScreen before
|
||||
// any session check, since neither flow requires an existing session. Session validity is
|
||||
// still decided by the server (GET /api/session/whoami against the rn_session cookie), not by
|
||||
// anything read from localStorage/sessionStorage.
|
||||
export default function PlatformShell() {
|
||||
const [student, setStudent] = useState(null)
|
||||
const [checking, setChecking] = useState(true)
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const [view, setView] = useState(
|
||||
params.get('activate') ? 'activate' : params.get('reset') ? 'reset' : 'login'
|
||||
)
|
||||
const [resetToken, setResetToken] = useState(params.get('reset') || null)
|
||||
|
||||
useEffect(() => {
|
||||
if (view === 'activate' || view === 'reset') { setChecking(false); return }
|
||||
const cached = getCachedDisplay()
|
||||
if (cached) setStudent(cached) // optimistic paint while we confirm with the server
|
||||
api.whoami()
|
||||
@@ -20,9 +31,29 @@ export default function PlatformShell() {
|
||||
})
|
||||
.catch(() => setStudent(null))
|
||||
.finally(() => setChecking(false))
|
||||
}, [])
|
||||
}, [view])
|
||||
|
||||
if (checking) return null
|
||||
if (!student) return <EntryScreen onLogin={setStudent} />
|
||||
return <StudentDashboard student={student} onLogout={() => setStudent(null)} />
|
||||
|
||||
if (view === 'activate') {
|
||||
return <SetPasswordScreen token={params.get('activate')} purpose="activation"
|
||||
onDone={(s) => { setStudent(s); setView('dashboard'); window.history.replaceState({}, '', '/') }}
|
||||
onCancel={() => { setView('login'); window.history.replaceState({}, '', '/') }} />
|
||||
}
|
||||
if (view === 'reset') {
|
||||
return <SetPasswordScreen token={resetToken} purpose="reset"
|
||||
onDone={() => { setView('login'); window.history.replaceState({}, '', '/') }}
|
||||
onCancel={() => { setView('login'); window.history.replaceState({}, '', '/') }} />
|
||||
}
|
||||
if (view === 'forgot') {
|
||||
return <ForgotPasswordScreen onCancel={() => setView('login')} onHaveToken={(t) => { setResetToken(t); setView('reset') }} />
|
||||
}
|
||||
if (view === 'pilot') {
|
||||
return <EntryScreen onLogin={(s) => { setStudent(s); setView('dashboard') }} />
|
||||
}
|
||||
if (!student) {
|
||||
return <LoginScreen onLogin={(s) => { setStudent(s); setView('dashboard') }}
|
||||
onShowPilotEntry={() => setView('pilot')} onShowForgotPassword={() => setView('forgot')} />
|
||||
}
|
||||
return <StudentDashboard student={student} onLogout={() => { setStudent(null); setView('login') }} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user