TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1"use client";2// Paramètres : profil (lecture), changement de mot de passe, thème,3// raccourcis clavier documentés, transparence (à propos).4import { useState } from "react";5import { Info, Keyboard, KeyRound, Palette, UserRound } from "lucide-react";6import { PageHeader } from "@/components/app-shell";7import { Button, Card, Input, Label, Spinner } from "@/components/ui";8import { ThemeToggle } from "@/components/theme-toggle";9import { ErrorBanner, postJson } from "./shared";1011const SHORTCUTS: { keys: string[]; description: string }[] = [12 { keys: ["⌘", "K"], description: "Nouvelle conversation dans le chat" },13 { keys: ["⌘", "J"], description: "Ouvrir le centre d'apprentissage" },14 { keys: ["Échap"], description: "Arrêter la réponse en cours (chat) ou fermer un panneau" },15 { keys: ["Entrée"], description: "Envoyer le message (Maj+Entrée : nouvelle ligne)" },16 { keys: ["Espace"], description: "Retourner la carte mémoire (flashcards)" },17 { keys: ["1", "–", "4"], description: "Noter la carte : Encore / Difficile / Bien / Facile" },18];1920export function SettingsApp({21 displayName,22 username,23 email,24 role,25}: {26 displayName: string;27 username: string;28 email: string | null;29 role: "student" | "instructor" | "admin";30}) {31 const [current, setCurrent] = useState("");32 const [next, setNext] = useState("");33 const [confirmPwd, setConfirmPwd] = useState("");34 const [busy, setBusy] = useState(false);35 const [error, setError] = useState<string | null>(null);36 const [success, setSuccess] = useState(false);3738 async function changePassword(e: React.FormEvent) {39 e.preventDefault();40 setError(null);41 setSuccess(false);42 if (next !== confirmPwd) {43 setError("La confirmation ne correspond pas au nouveau mot de passe.");44 return;45 }46 setBusy(true);47 try {48 await postJson("/api/auth/change-password", { currentPassword: current, newPassword: next });49 setSuccess(true);50 setCurrent("");51 setNext("");52 setConfirmPwd("");53 } catch (err) {54 setError(err instanceof Error ? err.message : "Erreur lors du changement de mot de passe.");55 } finally {56 setBusy(false);57 }58 }5960 const roleLabel = role === "student" ? "Étudiant·e" : role === "admin" ? "Administrateur" : "Professeur";6162 return (63 <main className="px-4 sm:px-6 py-6 max-w-2xl mx-auto w-full space-y-6">64 <PageHeader title="Paramètres" subtitle="Votre compte, votre affichage et le fonctionnement de la plateforme." />6566 {/* Profil */}67 <Card className="p-5">68 <h2 className="flex items-center gap-2 font-semibold text-fg mb-4"><UserRound size={17} className="text-brand-500" /> Profil</h2>69 <dl className="grid grid-cols-[130px_1fr] gap-y-2.5 text-sm">70 <dt className="text-muted">Nom affiché</dt>71 <dd className="text-fg font-medium">{displayName}</dd>72 <dt className="text-muted">Identifiant</dt>73 <dd className="text-fg font-mono text-[13px]">{username}</dd>74 {email && (75 <>76 <dt className="text-muted">Courriel</dt>77 <dd className="text-fg">{email}</dd>78 </>79 )}80 <dt className="text-muted">Rôle</dt>81 <dd className="text-fg">{roleLabel}</dd>82 </dl>83 <p className="text-[12px] text-muted mt-3">Pour modifier votre nom affiché ou vos cours, contactez votre professeur.</p>84 </Card>8586 {/* Mot de passe */}87 <Card className="p-5">88 <h2 className="flex items-center gap-2 font-semibold text-fg mb-4"><KeyRound size={17} className="text-brand-500" /> Mot de passe</h2>89 <form onSubmit={changePassword} className="space-y-3.5">90 <div>91 <Label htmlFor="pwd-current">Mot de passe actuel</Label>92 <Input93 id="pwd-current" type="password" required autoComplete="current-password"94 value={current} onChange={(e) => setCurrent(e.target.value)}95 />96 </div>97 <div className="grid sm:grid-cols-2 gap-3.5">98 <div>99 <Label htmlFor="pwd-new">Nouveau mot de passe</Label>100 <Input101 id="pwd-new" type="password" required autoComplete="new-password" minLength={8}102 value={next} onChange={(e) => setNext(e.target.value)}103 />104 </div>105 <div>106 <Label htmlFor="pwd-confirm">Confirmer le nouveau</Label>107 <Input108 id="pwd-confirm" type="password" required autoComplete="new-password"109 value={confirmPwd} onChange={(e) => setConfirmPwd(e.target.value)}110 />111 </div>112 </div>113 {error && <ErrorBanner message={error} />}114 {success && (115 <p className="text-sm text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 border border-emerald-500/25 rounded-xl px-4 py-3" role="status">116 Mot de passe modifié — vos autres sessions ont été déconnectées par sécurité.117 </p>118 )}119 <Button type="submit" disabled={busy || !current || !next || !confirmPwd}>120 {busy ? <Spinner /> : "Changer le mot de passe"}121 </Button>122 </form>123 </Card>124125 {/* Thème */}126 <Card className="p-5">127 <h2 className="flex items-center gap-2 font-semibold text-fg mb-3"><Palette size={17} className="text-brand-500" /> Thème</h2>128 <div className="flex items-center gap-3">129 <ThemeToggle className="border border-app bg-card" />130 <p className="text-[13px] text-muted">131 Cliquez pour alterner entre clair, sombre et système. « Système » suit automatiquement le réglage de votre132 appareil. Le choix est mémorisé sur cet appareil.133 </p>134 </div>135 </Card>136137 {/* Raccourcis clavier */}138 <Card className="p-5">139 <h2 className="flex items-center gap-2 font-semibold text-fg mb-4"><Keyboard size={17} className="text-brand-500" /> Raccourcis clavier</h2>140 <ul className="space-y-2.5">141 {SHORTCUTS.map((s, i) => (142 <li key={i} className="flex items-center gap-3 text-sm">143 <span className="flex gap-1 shrink-0 w-24">144 {s.keys.map((k, j) => (145 <kbd key={j} className="px-1.5 py-0.5 rounded-md border border-app bg-surface-2 dark:bg-brand-900/50 text-[11.5px] font-semibold text-fg font-mono">146 {k}147 </kbd>148 ))}149 </span>150 <span className="text-fg">{s.description}</span>151 </li>152 ))}153 </ul>154 </Card>155156 {/* À propos / transparence */}157 <Card className="p-5">158 <h2 className="flex items-center gap-2 font-semibold text-fg mb-4"><Info size={17} className="text-brand-500" /> À propos et transparence</h2>159 <div className="space-y-4 text-[13.5px] leading-relaxed text-fg">160 <div>161 <h3 className="font-semibold mb-1">Modes de connaissances</h3>162 <p className="text-muted">163 <strong className="text-fg">Cours uniquement</strong> (par défaut) : les réponses s'appuient exclusivement sur le164 matériel officiel des cours, avec les diapositives citées.{" "}165 <strong className="text-fg">Cours + général</strong> : le matériel cité est complété par les connaissances du166 modèle, clairement distinguées.{" "}167 <strong className="text-fg">Général</strong> : réponse sans le matériel du cours, toujours signalée comme telle.168 </p>169 </div>170 <div>171 <h3 className="font-semibold mb-1">Origine des données</h3>172 <p className="text-muted">173 Le contenu pédagogique provient des documents officiels d'IMM1003 et IMM1033 (UQO), indexés localement.174 Vos activités (cartes, quiz, examens) restent sur le serveur du cours et servent uniquement à estimer votre175 maîtrise et à personnaliser les recommandations. La maîtrise affichée est une <em>estimation</em>, jamais une note.176 </p>177 </div>178 <div>179 <h3 className="font-semibold mb-1">Budget d'usage</h3>180 <p className="text-muted">181 Les fonctions d'IA (chat, résumés, génération de cartes) consomment un budget partagé plafonné par personne et182 par jour, pour garder la plateforme équitable et durable. Si vous atteignez la limite, un message vous l'indique —183 les révisions (flashcards, quiz, examens) restent toujours disponibles, car elles ne consomment pas d'IA.184 </p>185 </div>186 <div>187 <h3 className="font-semibold mb-1">Limites</h3>188 <p className="text-muted">189 Immbot AI peut se tromper. Vérifiez les sources citées, signalez les erreurs avec l'icône drapeau du chat, et190 référez-vous aux documents officiels du cours en cas de doute.191 </p>192 </div>193 </div>194 </Card>195 </main>196 );197}198