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:
@@ -0,0 +1,59 @@
|
||||
import { useState } from 'react'
|
||||
import { api } from './api'
|
||||
import styles from './Platform.module.css'
|
||||
|
||||
// Phase 5A §6/§9 — self-service reset REQUEST. No email delivery exists yet
|
||||
// (docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md §5) — this always shows the same generic
|
||||
// message regardless of whether the identifier exists (anti-enumeration, matches the server's
|
||||
// own response shape exactly). In dev/test, the server includes dev_reset_token in its
|
||||
// response so this screen can offer a direct link — a real deployment omits it, and completing
|
||||
// the flow needs an instructor to run the admin-side reset instead (see
|
||||
// docs/RUNBOOK_RESET_STUDENT_PASSWORD.md).
|
||||
export default function ForgotPasswordScreen({ onCancel, onHaveToken }) {
|
||||
const [identifier, setIdentifier] = useState('')
|
||||
const [sent, setSent] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const submit = async (e) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await api.requestPasswordReset(identifier)
|
||||
setSent(result)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.entryWrap}>
|
||||
<div className={styles.entryCard}>
|
||||
<div className={styles.brand}>
|
||||
<span className={styles.brandIcon}>⎈</span> RootNode Academy
|
||||
</div>
|
||||
<p className={styles.tagline}>Reset your password</p>
|
||||
|
||||
{!sent ? (
|
||||
<form onSubmit={submit}>
|
||||
<label className={styles.label}>Email or login</label>
|
||||
<input className={styles.input} type="text" value={identifier} onChange={(e) => setIdentifier(e.target.value)} required />
|
||||
<button className={styles.primaryBtn} type="submit" disabled={loading}>{loading ? 'Sending…' : 'Send reset link'}</button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<p style={{ color: 'var(--text-2)', fontSize: '0.9rem' }}>{sent.message}</p>
|
||||
{sent.dev_reset_token && (
|
||||
<p style={{ fontSize: '0.8rem', color: 'var(--text-3)' }}>
|
||||
Dev/test only (never shown in production):{' '}
|
||||
<button className={styles.linkBtn} onClick={() => onHaveToken(sent.dev_reset_token)}>use this reset token</button>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<p style={{ marginTop: '1rem', textAlign: 'center' }}>
|
||||
<button className={styles.linkBtn} onClick={onCancel}>Back to sign in</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user