This commit is contained in:
2026-04-25 20:25:47 +02:00
parent 4e26248dc8
commit bd7c67fc93
3 changed files with 174 additions and 5 deletions

View File

@@ -30,13 +30,19 @@ async function parseApiResponse(res) {
return data
}
async function apiFetch(path, options) {
const res = await fetch(`${API_BASE}${path}`, options)
async function apiFetch(path, options = {}) {
const res = await fetch(`${API_BASE}${path}`, {
credentials: 'same-origin',
...options,
})
return parseApiResponse(res)
}
async function fetchAudio(path, options) {
const res = await fetch(`${API_BASE}${path}`, options)
const res = await fetch(`${API_BASE}${path}`, {
credentials: 'same-origin',
...options,
})
if (!res.ok) {
let detail = `Erreur API (${res.status})`
const contentType = res.headers.get('content-type') || ''
@@ -99,7 +105,72 @@ function getSupportedRecordingMimeType() {
return candidates.find((type) => MediaRecorder.isTypeSupported(type)) || ''
}
export default function App() {
function LoginPage({ onLogin }) {
const [credentials, setCredentials] = useState({ username: '', password: '' })
const [status, setStatus] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
async function submitLogin(e) {
e.preventDefault()
if (!credentials.username.trim() || !credentials.password) return
setIsSubmitting(true)
setStatus('')
try {
const data = await apiFetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: credentials.username.trim(),
password: credentials.password,
}),
})
onLogin(data.user)
} catch (error) {
setStatus(error.message || 'Connexion impossible.')
} finally {
setIsSubmitting(false)
}
}
return (
<main className="login-page">
<section className="login-panel card">
<Avatar speaking={false} />
<div className="login-copy">
<h1>Professeur TOP</h1>
<p className="muted">Connexion eleve, professeur ou maintenance.</p>
</div>
<form onSubmit={submitLogin} className="stack login-form">
<input
autoComplete="username"
placeholder="Identifiant"
value={credentials.username}
onChange={(event) =>
setCredentials({ ...credentials, username: event.target.value })
}
/>
<input
autoComplete="current-password"
placeholder="Mot de passe"
type="password"
value={credentials.password}
onChange={(event) =>
setCredentials({ ...credentials, password: event.target.value })
}
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Connexion...' : 'Se connecter'}
</button>
</form>
{status && <p className="muted voice-status">{status}</p>}
</section>
</main>
)
}
function TutorApp({ currentUser, onLogout }) {
const [students, setStudents] = useState([])
const [selectedStudentId, setSelectedStudentId] = useState('')
const [form, setForm] = useState({ first_name: '', age: 8, grade: 'CM1' })
@@ -725,6 +796,12 @@ export default function App() {
: 'Choisis ou crée un élève pour commencer.'}
</p>
</div>
<div className="session-user">
<span>{currentUser.username} · {currentUser.role}</span>
<button type="button" className="secondary-button" onClick={onLogout}>
Deconnexion
</button>
</div>
</div>
<section className="messages">
@@ -795,3 +872,53 @@ export default function App() {
</div>
)
}
export default function App() {
const [currentUser, setCurrentUser] = useState(null)
const [isCheckingSession, setIsCheckingSession] = useState(true)
useEffect(() => {
let isMounted = true
async function loadSession() {
try {
const user = await apiFetch('/auth/me')
if (isMounted) setCurrentUser(user)
} catch {
if (isMounted) setCurrentUser(null)
} finally {
if (isMounted) setIsCheckingSession(false)
}
}
loadSession()
return () => {
isMounted = false
}
}, [])
async function logout() {
try {
await apiFetch('/auth/logout', { method: 'POST' })
} finally {
setCurrentUser(null)
}
}
if (isCheckingSession) {
return (
<main className="login-page">
<section className="login-panel card">
<Avatar speaking={false} />
<p className="muted">Verification de la session...</p>
</section>
</main>
)
}
if (!currentUser) {
return <LoginPage onLogin={setCurrentUser} />
}
return <TutorApp currentUser={currentUser} onLogout={logout} />
}

View File

@@ -19,6 +19,10 @@ button {
cursor: pointer;
}
button:disabled { opacity: 0.5; cursor: not-allowed; }
.secondary-button {
background: #e2e8f0;
color: #14213d;
}
.layout {
display: grid;
@@ -36,7 +40,15 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
.sidebar, .main { display: flex; flex-direction: column; gap: 1rem; }
.stack { display: flex; flex-direction: column; gap: 0.75rem; }
.small-gap { gap: 0.5rem; }
.hero { display: flex; align-items: center; gap: 1rem; }
.hero { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.session-user {
margin-left: auto;
display: flex;
align-items: center;
gap: 0.75rem;
color: #64748b;
font-size: 0.95rem;
}
.messages {
flex: 1;
min-height: 360px;
@@ -113,6 +125,28 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
display: block;
}
.avatar-caption { font-weight: 700; }
.login-page {
min-height: 100vh;
display: grid;
place-items: center;
padding: 1rem;
}
.login-panel {
width: min(420px, 100%);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.login-copy {
text-align: center;
}
.login-copy h1 {
margin: 0;
}
.login-form {
width: 100%;
}
@keyframes pulse {
from { transform: scale(1); }
@@ -122,4 +156,10 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
@media (max-width: 920px) {
.layout { grid-template-columns: 1fr; }
.composer { grid-template-columns: 1fr; }
.hero { align-items: flex-start; }
.session-user {
width: 100%;
margin-left: 0;
justify-content: space-between;
}
}