- 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.
64 lines
2.4 KiB
React
64 lines
2.4 KiB
React
import { useState } from 'react'
|
|
import { api } from './api'
|
|
import styles from './Platform.module.css'
|
|
|
|
// Phase 5A §6 — the real, credentialed student login. This is now the default entry point
|
|
// (see PlatformShell.jsx); EntryScreen (pilot, no password) is still reachable for
|
|
// not-yet-converted accounts via the link at the bottom, not removed — see
|
|
// docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md §7.
|
|
export default function LoginScreen({ onLogin, onShowPilotEntry, onShowForgotPassword }) {
|
|
const [identifier, setIdentifier] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState(null)
|
|
|
|
const handleLogin = async (e) => {
|
|
e.preventDefault()
|
|
if (!identifier || !password) return
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const student = await api.studentLogin(identifier, password)
|
|
onLogin(student)
|
|
} catch (err) {
|
|
setError(err.message)
|
|
} 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}>Sign in to continue your learning.</p>
|
|
|
|
{error && <div className={styles.errorBox}>{error}</div>}
|
|
|
|
<form onSubmit={handleLogin}>
|
|
<label className={styles.label}>Email or login</label>
|
|
<input className={styles.input} type="text" autoComplete="username" value={identifier}
|
|
onChange={(e) => setIdentifier(e.target.value)} required />
|
|
|
|
<label className={styles.label}>Password</label>
|
|
<input className={styles.input} type="password" autoComplete="current-password" value={password}
|
|
onChange={(e) => setPassword(e.target.value)} required />
|
|
|
|
<button className={styles.primaryBtn} type="submit" disabled={loading}>
|
|
{loading ? 'Signing in…' : 'Sign in'}
|
|
</button>
|
|
</form>
|
|
|
|
<p style={{ marginTop: '1rem', textAlign: 'center' }}>
|
|
<button className={styles.linkBtn} onClick={onShowForgotPassword}>Forgot password?</button>
|
|
</p>
|
|
<p style={{ marginTop: '0.5rem', textAlign: 'center' }}>
|
|
<button className={styles.linkBtn} onClick={onShowPilotEntry}>Pick your name instead (pilot cohorts)</button>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|