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:
2026-08-25 01:19:44 -05:00
parent ee8ccb2ab6
commit 5c7e49c399
9 changed files with 524 additions and 8 deletions
+36
View File
@@ -83,4 +83,40 @@ export const api = {
capstone: () => fetch('/api/capstone').then(json),
achievements: () => fetch('/api/achievements').then(json),
// ── Phase 5A: real student accounts ─────────────────────────────────────────────────────
// No CSRF token for login/activate/reset-request/reset — same reasoning as the pilot login
// above: nothing authenticated exists yet to forge into.
studentLogin: async (identifier, password) => {
const { student } = await fetch('/api/student/login', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier, password }),
}).then(json)
cacheDisplay(student)
return student
},
activate: async (token, password) => {
const { student } = await fetch('/api/student/activate', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, password }),
}).then(json)
cacheDisplay(student)
return student
},
credentialTokenPreview: (purpose, token) => fetch(`/api/student/credential-token/${purpose}?token=${encodeURIComponent(token)}`).then(json),
requestPasswordReset: (identifier) => fetch('/api/student/request-password-reset', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier }),
}).then(json),
resetPassword: (token, password) => fetch('/api/student/reset-password', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, password }),
}).then(json),
changePassword: (old_password, new_password) => mutate('/api/student/change-password', { old_password, new_password }),
profile: () => fetch('/api/student/profile').then(json),
// ── Phase 5A: remote classroom ───────────────────────────────────────────────────────────
liveClassroom: () => fetch('/api/classroom/live').then(json),
classroom: (id) => fetch(`/api/classroom/${id}`).then(json),
classroomAnnouncements: (id) => fetch(`/api/classroom/${id}/announcements`).then(json),
joinClassroom: (id) => mutate(`/api/classroom/${id}/join`),
leaveClassroom: (id) => mutate(`/api/classroom/${id}/leave`),
classroomHeartbeat: (id) => mutate(`/api/classroom/${id}/heartbeat`),
requestHelp: (id, category, message) => mutate(`/api/classroom/${id}/help-request`, { category, message }),
}