- 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.
77 lines
3.2 KiB
React
77 lines
3.2 KiB
React
import { useState, useEffect } from 'react'
|
|
import { api } from './api'
|
|
import styles from './Platform.module.css'
|
|
|
|
// Phase 5A §5/§6 — one screen for both flows the credential token mechanism serves:
|
|
// activation (first login, purpose='activation') and forgot-password (purpose='reset'). Same
|
|
// UI, different API call and success copy.
|
|
export default function SetPasswordScreen({ token, purpose, onDone, onCancel }) {
|
|
const [preview, setPreview] = useState(null)
|
|
const [previewError, setPreviewError] = useState(null)
|
|
const [password, setPassword] = useState('')
|
|
const [confirm, setConfirm] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState(null)
|
|
|
|
useEffect(() => {
|
|
api.credentialTokenPreview(purpose, token).then(setPreview).catch((e) => setPreviewError(e.message))
|
|
}, [token, purpose])
|
|
|
|
const submit = async (e) => {
|
|
e.preventDefault()
|
|
if (password.length < 10) { setError('Password must be at least 10 characters.'); return }
|
|
if (password !== confirm) { setError('Passwords do not match.'); return }
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
if (purpose === 'activation') {
|
|
const student = await api.activate(token, password)
|
|
onDone(student)
|
|
} else {
|
|
await api.resetPassword(token, password)
|
|
onDone(null) // reset doesn't log the browser in — every existing session was revoked, sign in fresh
|
|
}
|
|
} 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}>{purpose === 'activation' ? 'Set up your account' : 'Choose a new password'}</p>
|
|
|
|
{previewError && (
|
|
<div className={styles.errorBox}>
|
|
This link is invalid or has expired. Ask your instructor for a new one.
|
|
</div>
|
|
)}
|
|
{preview && <p style={{ color: 'var(--text-2)', fontSize: '0.9rem' }}>Hi, {preview.display_name}.</p>}
|
|
{error && <div className={styles.errorBox}>{error}</div>}
|
|
|
|
{!previewError && (
|
|
<form onSubmit={submit}>
|
|
<label className={styles.label}>New password</label>
|
|
<input className={styles.input} type="password" autoComplete="new-password" value={password}
|
|
onChange={(e) => setPassword(e.target.value)} required minLength={10} />
|
|
<label className={styles.label}>Confirm password</label>
|
|
<input className={styles.input} type="password" autoComplete="new-password" value={confirm}
|
|
onChange={(e) => setConfirm(e.target.value)} required minLength={10} />
|
|
<button className={styles.primaryBtn} type="submit" disabled={loading}>
|
|
{loading ? 'Saving…' : purpose === 'activation' ? 'Activate my account' : 'Set new password'}
|
|
</button>
|
|
</form>
|
|
)}
|
|
<p style={{ marginTop: '1rem', textAlign: 'center' }}>
|
|
<button className={styles.linkBtn} onClick={onCancel}>Back to sign in</button>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|