feat: Dashboard placeholder avec preview Score Sérénité

Permet aux parents de visualiser une démo du Score Sérénité dès leur
première connexion, avant même que les données réelles soient disponibles.
Les autres rôles (enseignant, élève, admin) ont également leur dashboard
adapté avec des sections placeholder.

La landing page redirige automatiquement vers /dashboard si l'utilisateur
est déjà authentifié, offrant un accès direct au tableau de bord.
This commit is contained in:
2026-02-04 18:34:08 +01:00
parent d3c6773be5
commit b45ef735db
26 changed files with 3096 additions and 76 deletions

View File

@@ -1,28 +1,208 @@
<script lang="ts">
let count = $state(0);
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { refreshToken, isAuthenticated } from '$lib/auth/auth.svelte';
function increment() {
count++;
let isChecking = $state(true);
// Check if user is authenticated and redirect to dashboard
$effect(() => {
if (browser) {
checkAuthAndRedirect();
}
});
async function checkAuthAndRedirect() {
// If already has token in memory, redirect immediately
if (isAuthenticated()) {
goto('/dashboard');
return;
}
// Try to refresh token from cookie
const refreshed = await refreshToken();
if (refreshed) {
goto('/dashboard');
return;
}
// Not authenticated, show landing page
isChecking = false;
}
function goToLogin() {
goto('/login');
}
</script>
<svelte:head>
<title>Classeo</title>
<title>Classeo - Application de gestion scolaire</title>
</svelte:head>
<main class="flex min-h-screen flex-col items-center justify-center bg-gray-50">
<div class="text-center">
<h1 class="mb-4 text-4xl font-bold text-primary">Bienvenue sur Classeo</h1>
<p class="mb-8 text-gray-600">Application de gestion scolaire</p>
<div class="rounded-lg bg-white p-8 shadow-md">
<p class="mb-4 text-2xl font-semibold text-gray-800">Compteur: {count}</p>
<button
onclick={increment}
class="rounded-md bg-primary px-6 py-2 text-primary-foreground transition-colors hover:bg-primary/90"
>
Incrementer
</button>
{#if isChecking}
<main class="landing">
<div class="loading">
<div class="spinner"></div>
<p>Chargement...</p>
</div>
</div>
</main>
</main>
{:else}
<main class="landing">
<div class="hero">
<h1>Bienvenue sur <span class="brand">Classeo</span></h1>
<p class="tagline">L'application de gestion scolaire qui simplifie le quotidien des familles et des enseignants</p>
<div class="cta">
<button class="btn-primary" onclick={goToLogin}>
Se connecter
</button>
</div>
<div class="features">
<div class="feature">
<span class="feature-icon">💚</span>
<h3>Score Serenite</h3>
<p>Suivez la scolarite de votre enfant en un coup d'oeil</p>
</div>
<div class="feature">
<span class="feature-icon">📅</span>
<h3>Emploi du temps</h3>
<p>Consultez les cours et les modifications en temps reel</p>
</div>
<div class="feature">
<span class="feature-icon">📝</span>
<h3>Notes et devoirs</h3>
<p>Restez informe des resultats et des travaux a faire</p>
</div>
</div>
</div>
</main>
{/if}
<style>
.landing {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8fafc 0%, #e0f2fe 100%);
padding: 2rem;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
color: #64748b;
}
.spinner {
width: 2rem;
height: 2rem;
border: 3px solid #e2e8f0;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.hero {
max-width: 800px;
text-align: center;
}
h1 {
font-size: 2.5rem;
font-weight: 700;
color: #1e293b;
margin: 0 0 1rem;
}
.brand {
color: #3b82f6;
}
.tagline {
font-size: 1.25rem;
color: #64748b;
margin: 0 0 2rem;
line-height: 1.6;
}
.cta {
margin-bottom: 3rem;
}
.btn-primary {
padding: 1rem 2.5rem;
font-size: 1.125rem;
font-weight: 600;
color: white;
background: #3b82f6;
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 14px rgba(59, 130, 246, 0.4);
}
.btn-primary:hover {
background: #2563eb;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.5);
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.feature {
padding: 1.5rem;
background: white;
border-radius: 1rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.feature-icon {
font-size: 2.5rem;
display: block;
margin-bottom: 0.75rem;
}
.feature h3 {
margin: 0 0 0.5rem;
font-size: 1.125rem;
font-weight: 600;
color: #1e293b;
}
.feature p {
margin: 0;
font-size: 0.875rem;
color: #64748b;
line-height: 1.5;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 640px) {
h1 {
font-size: 1.75rem;
}
.tagline {
font-size: 1rem;
}
.features {
gap: 1rem;
}
}
</style>

View File

@@ -0,0 +1,208 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { logout } from '$lib/auth/auth.svelte';
let { children } = $props();
let isLoggingOut = $state(false);
// Note: Authentication is handled by authenticatedFetch in the page component.
// If not authenticated, authenticatedFetch will attempt refresh and redirect to /login if needed.
async function handleLogout() {
isLoggingOut = true;
try {
await logout();
} finally {
isLoggingOut = false;
}
}
function goHome() {
goto('/dashboard');
}
function goSettings() {
goto('/settings');
}
</script>
<div class="dashboard-layout">
<header class="dashboard-header">
<div class="header-content">
<button class="logo-button" onclick={goHome}>
<span class="logo-text">Classeo</span>
</button>
<nav class="header-nav">
<a href="/dashboard" class="nav-link active">Tableau de bord</a>
<button class="nav-button" onclick={goSettings}>Parametres</button>
<button class="logout-button" onclick={handleLogout} disabled={isLoggingOut}>
{#if isLoggingOut}
<span class="spinner"></span>
Deconnexion...
{:else}
Deconnexion
{/if}
</button>
</nav>
</div>
</header>
<main class="dashboard-main">
<div class="main-content">
{@render children()}
</div>
</main>
</div>
<style>
.dashboard-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--surface-primary, #f8fafc);
}
.dashboard-header {
background: var(--surface-elevated, #fff);
border-bottom: 1px solid var(--border-subtle, #e2e8f0);
padding: 0 1.5rem;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
height: 64px;
}
.logo-button {
background: none;
border: none;
cursor: pointer;
padding: 0.5rem 0;
}
.logo-text {
font-size: 1.25rem;
font-weight: 700;
color: var(--accent-primary, #0ea5e9);
}
.header-nav {
display: flex;
align-items: center;
gap: 1rem;
}
.nav-link {
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary, #64748b);
text-decoration: none;
border-radius: 0.5rem;
transition: all 0.2s;
}
.nav-link:hover {
color: var(--text-primary, #1f2937);
background: var(--surface-primary, #f8fafc);
}
.nav-link.active {
color: var(--accent-primary, #0ea5e9);
background: var(--accent-primary-light, #e0f2fe);
}
.nav-button {
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary, #64748b);
background: transparent;
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.nav-button:hover {
color: var(--text-primary, #1f2937);
background: var(--surface-primary, #f8fafc);
}
.logout-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary, #64748b);
background: transparent;
border: 1px solid var(--border-subtle, #e2e8f0);
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.logout-button:hover:not(:disabled) {
color: var(--color-alert, #ef4444);
border-color: var(--color-alert, #ef4444);
}
.logout-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.spinner {
width: 14px;
height: 14px;
border: 2px solid var(--border-subtle, #e2e8f0);
border-top-color: var(--text-secondary, #64748b);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.dashboard-main {
flex: 1;
padding: 1.5rem;
}
.main-content {
max-width: 1200px;
margin: 0 auto;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 768px) {
.header-content {
flex-wrap: wrap;
height: auto;
padding: 0.75rem 0;
gap: 0.75rem;
}
.header-nav {
width: 100%;
justify-content: flex-end;
flex-wrap: wrap;
gap: 0.5rem;
}
.dashboard-main {
padding: 1rem;
}
}
</style>

View File

@@ -0,0 +1,118 @@
<script lang="ts">
import type { DemoData } from '$types';
import demoData from '$lib/data/demo-data.json';
import DashboardParent from '$lib/components/organisms/Dashboard/DashboardParent.svelte';
import DashboardTeacher from '$lib/components/organisms/Dashboard/DashboardTeacher.svelte';
import DashboardStudent from '$lib/components/organisms/Dashboard/DashboardStudent.svelte';
import DashboardAdmin from '$lib/components/organisms/Dashboard/DashboardAdmin.svelte';
type UserRole = 'parent' | 'teacher' | 'student' | 'admin' | 'direction';
// For now, default to parent role with demo data
// TODO: Fetch real user profile from /api/me when endpoint is implemented
let userRole = $state<UserRole>('parent');
// Simulated first login detection (in real app, this comes from API)
let isFirstLogin = $state(true);
// Serenity score preference (in real app, this is stored in backend)
let serenityEnabled = $state(true);
// Use demo data for now (no real data available yet)
const hasRealData = false;
// Demo child name for personalized messages
const childName = 'Emma';
function handleToggleSerenity(enabled: boolean) {
serenityEnabled = enabled;
// TODO: POST to /api/me/preferences when backend is ready
console.log('Serenity score preference updated:', enabled);
}
// Cast demo data to proper type
const typedDemoData = demoData as DemoData;
// Allow switching roles for demo purposes
function switchRole(role: UserRole) {
userRole = role;
isFirstLogin = false;
}
</script>
<svelte:head>
<title>Tableau de bord - Classeo</title>
</svelte:head>
<!-- Demo role switcher - TODO: Remove when real authentication is implemented -->
<!-- This will be hidden once we can determine user role from /api/me -->
<div class="demo-controls">
<span class="demo-label">Démo - Changer de rôle :</span>
<button class:active={userRole === 'parent'} onclick={() => switchRole('parent')}>Parent</button>
<button class:active={userRole === 'teacher'} onclick={() => switchRole('teacher')}>Enseignant</button>
<button class:active={userRole === 'student'} onclick={() => switchRole('student')}>Élève</button>
<button class:active={userRole === 'admin'} onclick={() => switchRole('admin')}>Admin</button>
</div>
{#if userRole === 'parent'}
<DashboardParent
demoData={typedDemoData}
{isFirstLogin}
isLoading={false}
{hasRealData}
{serenityEnabled}
{childName}
onToggleSerenity={handleToggleSerenity}
/>
{:else if userRole === 'teacher'}
<DashboardTeacher isLoading={false} {hasRealData} />
{:else if userRole === 'student'}
<DashboardStudent demoData={typedDemoData} isLoading={false} {hasRealData} isMinor={true} />
{:else if userRole === 'admin' || userRole === 'direction'}
<DashboardAdmin
isLoading={false}
{hasRealData}
establishmentName="École Alpha"
/>
{/if}
<style>
.demo-controls {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
margin-bottom: 1rem;
background: #fef3c7;
border: 1px solid #fcd34d;
border-radius: 0.5rem;
flex-wrap: wrap;
}
.demo-label {
font-size: 0.875rem;
font-weight: 500;
color: #92400e;
}
.demo-controls button {
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
background: white;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.15s;
}
.demo-controls button:hover {
background: #f3f4f6;
}
.demo-controls button.active {
background: #3b82f6;
border-color: #3b82f6;
color: white;
}
</style>

View File

@@ -111,7 +111,7 @@
if (result.success) {
// Rediriger vers le dashboard
goto('/');
goto('/dashboard');
} else if (result.error) {
// Gérer les différents types d'erreur
switch (result.error.type) {

View File

@@ -15,7 +15,7 @@
}
function goHome() {
goto('/');
goto('/dashboard');
}
</script>
@@ -26,6 +26,8 @@
<span class="logo-text">Classeo</span>
</button>
<nav class="header-nav">
<a href="/dashboard" class="nav-link">Tableau de bord</a>
<a href="/settings" class="nav-link active">Parametres</a>
<button
class="logout-button"
onclick={handleLogout}
@@ -33,9 +35,9 @@
>
{#if isLoggingOut}
<span class="spinner"></span>
Déconnexion...
Deconnexion...
{:else}
Déconnexion
Deconnexion
{/if}
</button>
</nav>
@@ -58,7 +60,10 @@
.settings-header {
background: var(--surface-elevated, #fff);
border-bottom: 1px solid var(--border-subtle, #e2e8f0);
padding: 0 24px;
padding: 0 1.5rem;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
@@ -74,39 +79,59 @@
background: none;
border: none;
cursor: pointer;
padding: 8px 0;
padding: 0.5rem 0;
}
.logo-text {
font-size: 20px;
font-size: 1.25rem;
font-weight: 700;
color: var(--accent-primary, hsl(199, 89%, 48%));
color: var(--accent-primary, #0ea5e9);
}
.header-nav {
display: flex;
align-items: center;
gap: 16px;
gap: 1rem;
}
.nav-link {
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary, #64748b);
text-decoration: none;
border-radius: 0.5rem;
transition: all 0.2s;
}
.nav-link:hover {
color: var(--text-primary, #1f2937);
background: var(--surface-primary, #f8fafc);
}
.nav-link.active {
color: var(--accent-primary, #0ea5e9);
background: var(--accent-primary-light, #e0f2fe);
}
.logout-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
font-size: 14px;
gap: 0.5rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary, #64748b);
background: transparent;
border: 1px solid var(--border-subtle, #e2e8f0);
border-radius: 8px;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.logout-button:hover:not(:disabled) {
color: var(--color-alert, hsl(0, 72%, 51%));
border-color: var(--color-alert, hsl(0, 72%, 51%));
color: var(--color-alert, #ef4444);
border-color: var(--color-alert, #ef4444);
}
.logout-button:disabled {
@@ -132,4 +157,20 @@
transform: rotate(360deg);
}
}
@media (max-width: 768px) {
.header-content {
flex-wrap: wrap;
height: auto;
padding: 0.75rem 0;
gap: 0.75rem;
}
.header-nav {
width: 100%;
justify-content: flex-end;
flex-wrap: wrap;
gap: 0.5rem;
}
}
</style>