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

@@ -0,0 +1,58 @@
<script lang="ts">
let { message = 'Chargement...' }: { message?: string } = $props();
</script>
<div class="skeleton-card" aria-busy="true" aria-live="polite">
<div class="skeleton-line wide"></div>
<div class="skeleton-line medium"></div>
<div class="skeleton-line short"></div>
{#if message}
<p class="skeleton-message">{message}</p>
{/if}
</div>
<style>
.skeleton-card {
padding: 1.5rem;
background: #f9fafb;
border-radius: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.skeleton-line {
height: 1rem;
background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
background-size: 200% 100%;
border-radius: 0.25rem;
animation: shimmer 1.5s infinite;
}
.skeleton-line.wide {
width: 100%;
}
.skeleton-line.medium {
width: 75%;
}
.skeleton-line.short {
width: 50%;
}
.skeleton-message {
margin: 0.5rem 0 0;
font-size: 0.875rem;
color: #9ca3af;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>

View File

@@ -0,0 +1,90 @@
<script lang="ts">
let {
items = 3,
message = 'Chargement...'
}: {
items?: number;
message?: string;
} = $props();
</script>
<div class="skeleton-list" aria-busy="true" aria-live="polite">
{#each Array(items) as _, i (i)}
<div class="skeleton-item">
<div class="skeleton-icon"></div>
<div class="skeleton-content">
<div class="skeleton-line wide"></div>
<div class="skeleton-line short"></div>
</div>
</div>
{/each}
{#if message}
<p class="skeleton-message">{message}</p>
{/if}
</div>
<style>
.skeleton-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.skeleton-item {
display: flex;
gap: 1rem;
padding: 1rem;
background: #f9fafb;
border-radius: 0.5rem;
}
.skeleton-icon {
width: 2.5rem;
height: 2.5rem;
background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
background-size: 200% 100%;
border-radius: 0.5rem;
animation: shimmer 1.5s infinite;
flex-shrink: 0;
}
.skeleton-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: center;
}
.skeleton-line {
height: 0.75rem;
background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
background-size: 200% 100%;
border-radius: 0.25rem;
animation: shimmer 1.5s infinite;
}
.skeleton-line.wide {
width: 80%;
}
.skeleton-line.short {
width: 50%;
}
.skeleton-message {
margin: 0;
font-size: 0.875rem;
color: #9ca3af;
text-align: center;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>

View File

@@ -0,0 +1,91 @@
<script lang="ts">
import type { Snippet } from 'svelte';
let {
title,
subtitle,
isPlaceholder = false,
placeholderMessage = 'Bientôt disponible',
children
}: {
title: string;
subtitle?: string | undefined;
isPlaceholder?: boolean;
placeholderMessage?: string;
children?: Snippet;
} = $props();
</script>
<section class="dashboard-section" class:placeholder={isPlaceholder}>
<header class="section-header">
<h2 class="section-title">{title}</h2>
{#if subtitle}
<p class="section-subtitle">{subtitle}</p>
{/if}
</header>
<div class="section-content">
{#if isPlaceholder}
<div class="placeholder-content">
<span class="placeholder-icon">🚧</span>
<p class="placeholder-message">{placeholderMessage}</p>
</div>
{:else if children}
{@render children()}
{/if}
</div>
</section>
<style>
.dashboard-section {
background: white;
border-radius: 1rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.section-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.section-title {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: #1f2937;
}
.section-subtitle {
margin: 0.25rem 0 0;
font-size: 0.875rem;
color: #6b7280;
}
.section-content {
padding: 1.5rem;
}
.placeholder .section-content {
background: #f9fafb;
}
.placeholder-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
padding: 2rem;
text-align: center;
}
.placeholder-icon {
font-size: 2rem;
}
.placeholder-message {
margin: 0;
color: #6b7280;
font-size: 0.875rem;
}
</style>

View File

@@ -0,0 +1,449 @@
<script lang="ts">
import type { SerenityScore } from '$types';
import { getSerenityEmoji, getSerenityLabel } from '$lib/features/dashboard/serenity-score';
let {
score,
isEnabled = false,
isDemo = false,
onClose,
onToggleOptIn
}: {
score: SerenityScore;
isEnabled?: boolean;
isDemo?: boolean;
onClose: () => void;
onToggleOptIn?: ((enabled: boolean) => void) | undefined;
} = $props();
let localEnabled = $state(isEnabled);
// Sync local state with parent prop changes
$effect(() => {
localEnabled = isEnabled;
});
function handleToggle() {
localEnabled = !localEnabled;
onToggleOptIn?.(localEnabled);
}
function handleKeydown(event: globalThis.KeyboardEvent) {
if (event.key === 'Escape') {
onClose();
}
}
function handleBackdropClick(event: globalThis.MouseEvent) {
if (event.target === event.currentTarget) {
onClose();
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div class="modal-backdrop" onclick={handleBackdropClick}>
<div class="modal-content" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<header class="modal-header">
<h2 id="modal-title">Comment fonctionne le Score Sérénité ?</h2>
<button type="button" class="close-button" onclick={onClose} aria-label="Fermer">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</header>
<div class="modal-body">
{#if isDemo}
<div class="demo-notice">
<span class="demo-icon"></span>
<p>Ces données sont des exemples. Votre Score Sérénité sera calculé à partir des vraies données scolaires de votre enfant.</p>
</div>
{/if}
<section class="score-overview">
<div class="current-score">
<span class="emoji">{score.emoji}</span>
<span class="value">{score.value}</span>
<span class="label">{getSerenityLabel(score.value)}</span>
</div>
</section>
<section class="calculation-section">
<h3>Calcul du score</h3>
<p class="formula">Score = (Notes × 40%) + (Absences × 30%) + (Devoirs × 30%)</p>
<div class="components-grid">
<div class="component-card">
<div class="component-header">
<span class="component-emoji">{getSerenityEmoji(score.components.notes.score)}</span>
<span class="component-name">Notes</span>
<span class="component-weight">40%</span>
</div>
<div class="component-score">{score.components.notes.score}/100</div>
<p class="component-label">{score.components.notes.label}</p>
</div>
<div class="component-card">
<div class="component-header">
<span class="component-emoji">{getSerenityEmoji(score.components.absences.score)}</span>
<span class="component-name">Absences</span>
<span class="component-weight">30%</span>
</div>
<div class="component-score">{score.components.absences.score}/100</div>
<p class="component-label">{score.components.absences.label}</p>
</div>
<div class="component-card">
<div class="component-header">
<span class="component-emoji">{getSerenityEmoji(score.components.devoirs.score)}</span>
<span class="component-name">Devoirs</span>
<span class="component-weight">30%</span>
</div>
<div class="component-score">{score.components.devoirs.score}/100</div>
<p class="component-label">{score.components.devoirs.label}</p>
</div>
</div>
</section>
<section class="legend-section">
<h3>Légende</h3>
<div class="legend-items">
<div class="legend-item">
<span class="legend-emoji">💚</span>
<span class="legend-range">70-100</span>
<span class="legend-label">Excellent</span>
</div>
<div class="legend-item">
<span class="legend-emoji">🟡</span>
<span class="legend-range">40-69</span>
<span class="legend-label">A surveiller</span>
</div>
<div class="legend-item">
<span class="legend-emoji">🔴</span>
<span class="legend-range">0-39</span>
<span class="legend-label">Attention requise</span>
</div>
</div>
</section>
{#if onToggleOptIn}
<section class="opt-in-section">
<h3>Paramètres</h3>
<label class="toggle-label">
<span class="toggle-text">Activer le Score Sérénité</span>
<button
type="button"
class="toggle-switch"
class:enabled={localEnabled}
onclick={handleToggle}
role="switch"
aria-checked={localEnabled}
aria-label="Activer le Score Sérénité"
>
<span class="toggle-knob"></span>
</button>
</label>
<p class="opt-in-description">
{#if localEnabled}
Le Score Sérénité est actif. Vous recevrez des mises à jour régulières.
{:else}
Activez le Score Sérénité pour suivre la progression scolaire de votre enfant en un coup d'œil.
{/if}
</p>
</section>
{/if}
</div>
<footer class="modal-footer">
<button type="button" class="btn-primary" onclick={onClose}>Compris</button>
</footer>
</div>
</div>
<style>
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal-content {
background: white;
border-radius: 1rem;
max-width: 600px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.modal-header h2 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #1f2937;
}
.close-button {
padding: 0.5rem;
background: none;
border: none;
border-radius: 0.5rem;
cursor: pointer;
color: #6b7280;
transition: color 0.2s, background 0.2s;
}
.close-button:hover {
color: #1f2937;
background: #f3f4f6;
}
.modal-body {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.demo-notice {
display: flex;
gap: 0.75rem;
padding: 1rem;
background: #fef3c7;
border-radius: 0.5rem;
color: #92400e;
}
.demo-notice p {
margin: 0;
font-size: 0.875rem;
}
.score-overview {
display: flex;
justify-content: center;
}
.current-score {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.current-score .emoji {
font-size: 4rem;
}
.current-score .value {
font-size: 2.5rem;
font-weight: 700;
color: #1f2937;
}
.current-score .label {
font-size: 1rem;
color: #6b7280;
}
.calculation-section h3,
.legend-section h3,
.opt-in-section h3 {
margin: 0 0 1rem;
font-size: 1rem;
font-weight: 600;
color: #374151;
}
.formula {
margin: 0 0 1rem;
padding: 1rem;
background: #f9fafb;
border-radius: 0.5rem;
font-family: monospace;
font-size: 0.875rem;
color: #4b5563;
text-align: center;
}
.components-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
}
.component-card {
padding: 1rem;
background: #f9fafb;
border-radius: 0.5rem;
text-align: center;
}
.component-header {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.component-emoji {
font-size: 1.25rem;
}
.component-name {
font-weight: 600;
color: #374151;
}
.component-weight {
font-size: 0.75rem;
padding: 0.125rem 0.375rem;
background: #e5e7eb;
border-radius: 0.25rem;
color: #6b7280;
}
.component-score {
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
}
.component-label {
margin: 0.5rem 0 0;
font-size: 0.75rem;
color: #6b7280;
}
.legend-items {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: #f9fafb;
border-radius: 0.5rem;
}
.legend-emoji {
font-size: 1.25rem;
}
.legend-range {
font-weight: 600;
color: #374151;
}
.legend-label {
color: #6b7280;
}
.opt-in-section {
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
}
.toggle-label {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.toggle-text {
font-weight: 500;
color: #374151;
}
.toggle-switch {
position: relative;
width: 3rem;
height: 1.5rem;
background: #d1d5db;
border: none;
border-radius: 0.75rem;
cursor: pointer;
transition: background 0.2s;
}
.toggle-switch.enabled {
background: #22c55e;
}
.toggle-knob {
position: absolute;
top: 0.125rem;
left: 0.125rem;
width: 1.25rem;
height: 1.25rem;
background: white;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
transition: transform 0.2s;
}
.toggle-switch.enabled .toggle-knob {
transform: translateX(1.5rem);
}
.opt-in-description {
margin: 0.75rem 0 0;
font-size: 0.875rem;
color: #6b7280;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: flex-end;
}
.btn-primary {
padding: 0.75rem 1.5rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover {
background: #2563eb;
}
</style>

View File

@@ -0,0 +1,137 @@
<script lang="ts">
type Emoji = '💚' | '🟡' | '🔴';
let {
value,
emoji,
isDemo = false,
onClick
}: {
value: number;
emoji: Emoji;
isDemo?: boolean;
onClick?: () => void;
} = $props();
let isHovered = $state(false);
</script>
<button
type="button"
class="serenity-card"
class:hovered={isHovered}
onclick={onClick}
onmouseenter={() => (isHovered = true)}
onmouseleave={() => (isHovered = false)}
aria-label="Score Sérénité - Cliquez pour plus de détails"
>
<div class="card-header">
<h3 class="title">Score Sérénité</h3>
{#if isDemo}
<span class="demo-badge">Données de démonstration</span>
{/if}
</div>
<div class="score-display">
<span class="emoji" class:animated={isHovered}>{emoji}</span>
<span class="value">{value}</span>
<span class="max">/100</span>
</div>
<p class="hint">Cliquez pour en savoir plus</p>
</button>
<style>
.serenity-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 1.5rem;
background: var(--surface-elevated, #ffffff);
border: 1px solid var(--border-subtle, #e5e7eb);
border-radius: 1rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
width: 100%;
text-align: center;
}
.serenity-card:hover,
.serenity-card.hovered {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.serenity-card:focus-visible {
outline: 2px solid var(--accent-primary, #0ea5e9);
outline-offset: 2px;
}
.card-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
width: 100%;
}
.title {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--text-primary, #1f2937);
}
.demo-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
background: var(--color-attention, #f59e0b);
color: white;
border-radius: 0.25rem;
font-weight: 500;
}
.score-display {
display: flex;
align-items: baseline;
gap: 0.5rem;
}
.emoji {
font-size: 3rem;
transition: transform 0.3s ease;
}
.emoji.animated {
animation: bounce 0.5s ease;
}
.value {
font-size: 3rem;
font-weight: 700;
color: var(--text-primary, #1f2937);
}
.max {
font-size: 1.25rem;
color: var(--text-secondary, #6b7280);
}
.hint {
margin: 0;
font-size: 0.875rem;
color: var(--text-secondary, #6b7280);
}
@keyframes bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
</style>

View File

@@ -0,0 +1,233 @@
<script lang="ts">
import DashboardSection from '$lib/components/molecules/DashboardSection.svelte';
import SkeletonList from '$lib/components/atoms/Skeleton/SkeletonList.svelte';
let {
isLoading = false,
hasRealData = false,
establishmentName = ''
}: {
isLoading?: boolean;
hasRealData?: boolean;
establishmentName?: string;
} = $props();
</script>
<div class="dashboard-admin">
<header class="dashboard-header">
<h1>Administration</h1>
{#if establishmentName}
<p class="dashboard-subtitle">{establishmentName}</p>
{:else}
<p class="dashboard-subtitle">Bienvenue dans votre espace d'administration</p>
{/if}
</header>
<div class="quick-actions">
<h2 class="sr-only">Actions de configuration</h2>
<div class="action-cards">
<div class="action-card disabled" aria-disabled="true">
<span class="action-icon">👥</span>
<span class="action-label">Gérer les utilisateurs</span>
<span class="action-hint">Bientôt disponible</span>
</div>
<div class="action-card disabled" aria-disabled="true">
<span class="action-icon">🏫</span>
<span class="action-label">Configurer les classes</span>
<span class="action-hint">Bientôt disponible</span>
</div>
<div class="action-card disabled" aria-disabled="true">
<span class="action-icon">📅</span>
<span class="action-label">Calendrier scolaire</span>
<span class="action-hint">Bientôt disponible</span>
</div>
<div class="action-card disabled" aria-disabled="true">
<span class="action-icon">📤</span>
<span class="action-label">Importer des données</span>
<span class="action-hint">Bientôt disponible</span>
</div>
</div>
</div>
<div class="dashboard-grid">
<DashboardSection
title="Utilisateurs"
isPlaceholder={!hasRealData}
placeholderMessage="Les statistiques utilisateurs s'afficheront une fois les comptes créés"
>
{#if hasRealData}
{#if isLoading}
<SkeletonList items={3} message="Chargement des statistiques..." />
{:else}
<div class="stats-grid">
<div class="stat-card">
<span class="stat-value">--</span>
<span class="stat-label">Élèves</span>
</div>
<div class="stat-card">
<span class="stat-value">--</span>
<span class="stat-label">Enseignants</span>
</div>
<div class="stat-card">
<span class="stat-value">--</span>
<span class="stat-label">Parents</span>
</div>
</div>
{/if}
{/if}
</DashboardSection>
<DashboardSection
title="Configuration"
isPlaceholder={!hasRealData}
placeholderMessage="L'état de la configuration sera affiché ici"
>
{#if hasRealData && isLoading}
<SkeletonList items={4} message="Vérification de la configuration..." />
{/if}
</DashboardSection>
<DashboardSection
title="Activité récente"
isPlaceholder={!hasRealData}
placeholderMessage="L'historique des actions s'affichera ici"
>
{#if hasRealData && isLoading}
<SkeletonList items={5} message="Chargement de l'activité..." />
{/if}
</DashboardSection>
</div>
</div>
<style>
.dashboard-admin {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.dashboard-header {
margin-bottom: 0.5rem;
}
.dashboard-header h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
}
.dashboard-subtitle {
margin: 0.25rem 0 0;
color: #6b7280;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.quick-actions {
margin-bottom: 0.5rem;
}
.action-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
}
.action-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1.25rem 1rem;
background: white;
border: 2px solid #e5e7eb;
border-radius: 0.75rem;
text-decoration: none;
color: inherit;
transition: all 0.2s;
}
.action-card:not(.disabled):hover {
border-color: #3b82f6;
background: #eff6ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.action-card.disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-icon {
font-size: 2rem;
}
.action-label {
font-weight: 600;
color: #374151;
text-align: center;
}
.action-hint {
font-size: 0.75rem;
color: #6b7280;
text-align: center;
}
.dashboard-grid {
display: grid;
gap: 1.5rem;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.dashboard-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.dashboard-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.stat-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 1rem;
background: #f9fafb;
border-radius: 0.5rem;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
}
.stat-label {
font-size: 0.75rem;
color: #6b7280;
}
</style>

View File

@@ -0,0 +1,359 @@
<script lang="ts">
import type { DemoData } from '$types';
import SerenityScorePreview from '$lib/components/molecules/SerenityScore/SerenityScorePreview.svelte';
import SerenityScoreExplainer from '$lib/components/molecules/SerenityScore/SerenityScoreExplainer.svelte';
import DashboardSection from '$lib/components/molecules/DashboardSection.svelte';
import SkeletonCard from '$lib/components/atoms/Skeleton/SkeletonCard.svelte';
import SkeletonList from '$lib/components/atoms/Skeleton/SkeletonList.svelte';
import type { SerenityEmoji } from '$lib/features/dashboard/serenity-score';
let {
demoData,
isFirstLogin = false,
isLoading = false,
hasRealData = false,
serenityEnabled = false,
childName = '',
onToggleSerenity
}: {
demoData: DemoData;
isFirstLogin?: boolean;
isLoading?: boolean;
hasRealData?: boolean;
serenityEnabled?: boolean;
childName?: string;
onToggleSerenity?: (enabled: boolean) => void;
} = $props();
let showExplainer = $state(false);
const isDemo = $derived(!hasRealData);
function openExplainer() {
showExplainer = true;
}
function closeExplainer() {
showExplainer = false;
}
</script>
<div class="dashboard-parent">
{#if isFirstLogin}
<div class="onboarding-banner">
<div class="onboarding-content">
<span class="onboarding-icon">👋</span>
<div class="onboarding-text">
<h2>Bienvenue sur Classeo !</h2>
<p>
Découvrez le Score Sérénité, un indicateur unique qui vous donne en un coup d'œil une vision globale de la scolarité de votre enfant.
{#if isDemo}
Les données actuelles sont des exemples. Vos vraies données apparaîtront bientôt.
{/if}
</p>
</div>
</div>
</div>
{/if}
<div class="dashboard-grid">
<!-- Score Serenite - Always visible as demo or real -->
<div class="score-section">
{#if isLoading}
<SkeletonCard message={childName ? `Calcul du Score Sérénité de ${childName}...` : 'Calcul du Score Sérénité...'} />
{:else}
<SerenityScorePreview
value={demoData.serenityScore.value}
emoji={demoData.serenityScore.emoji as SerenityEmoji}
isDemo={isDemo}
onClick={openExplainer}
/>
{/if}
</div>
<!-- EDT Section -->
<DashboardSection
title="Emploi du temps"
subtitle={hasRealData ? "Aujourd'hui" : undefined}
isPlaceholder={!hasRealData}
placeholderMessage="L'emploi du temps sera disponible une fois les cours configurés"
>
{#if hasRealData}
{#if isLoading}
<SkeletonList items={4} message={childName ? `Chargement de l'emploi du temps de ${childName}...` : "Chargement de l'emploi du temps..."} />
{:else}
<ul class="schedule-list">
{#each demoData.schedule.today as item}
<li class="schedule-item">
<span class="schedule-time">{item.time}</span>
<span class="schedule-subject">{item.subject}</span>
<span class="schedule-room">{item.room}</span>
</li>
{/each}
</ul>
{/if}
{/if}
</DashboardSection>
<!-- Notes Section -->
<DashboardSection
title="Notes récentes"
isPlaceholder={!hasRealData}
placeholderMessage="Les notes apparaîtront ici dès qu'elles seront saisies"
>
{#if hasRealData}
{#if isLoading}
<SkeletonList items={3} message="Récupération des notes..." />
{:else}
<ul class="grades-list">
{#each demoData.grades.recent as grade}
<li class="grade-item">
<span class="grade-subject">{grade.subject}</span>
<span class="grade-value">{grade.value}/{grade.max}</span>
<span class="grade-eval">{grade.evaluation}</span>
</li>
{/each}
</ul>
{/if}
{/if}
</DashboardSection>
<!-- Devoirs Section -->
<DashboardSection
title="Devoirs à venir"
isPlaceholder={!hasRealData}
placeholderMessage="Les devoirs seront affichés ici une fois assignés"
>
{#if hasRealData}
{#if isLoading}
<SkeletonList items={3} message="Chargement des devoirs..." />
{:else}
<ul class="homework-list">
{#each demoData.homework.upcoming as homework}
<li class="homework-item" class:done={homework.status === 'done'}>
<span class="homework-subject">{homework.subject}</span>
<span class="homework-title">{homework.title}</span>
<span class="homework-due">Pour le {homework.dueDate}</span>
{#if homework.status === 'done'}
<span class="homework-status done">✓ Fait</span>
{:else if homework.status === 'late'}
<span class="homework-status late">En retard</span>
{/if}
</li>
{/each}
</ul>
{/if}
{/if}
</DashboardSection>
</div>
</div>
{#if showExplainer}
<SerenityScoreExplainer
score={demoData.serenityScore}
isEnabled={serenityEnabled}
isDemo={isDemo}
onClose={closeExplainer}
onToggleOptIn={onToggleSerenity}
/>
{/if}
<style>
.dashboard-parent {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.onboarding-banner {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border-radius: 1rem;
color: white;
overflow: hidden;
}
.onboarding-content {
display: flex;
gap: 1rem;
padding: 1.5rem;
align-items: flex-start;
}
.onboarding-icon {
font-size: 2rem;
flex-shrink: 0;
}
.onboarding-text h2 {
margin: 0 0 0.5rem;
font-size: 1.25rem;
font-weight: 600;
}
.onboarding-text p {
margin: 0;
font-size: 0.875rem;
opacity: 0.9;
line-height: 1.5;
}
.dashboard-grid {
display: grid;
gap: 1.5rem;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.dashboard-grid {
grid-template-columns: repeat(2, 1fr);
}
.score-section {
grid-column: 1 / -1;
}
}
@media (min-width: 1024px) {
.dashboard-grid {
grid-template-columns: repeat(3, 1fr);
}
.score-section {
grid-column: 1 / 2;
}
}
.score-section {
display: flex;
}
/* Schedule List */
.schedule-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.schedule-item {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 1rem;
padding: 0.75rem;
background: #f9fafb;
border-radius: 0.5rem;
align-items: center;
}
.schedule-time {
font-weight: 600;
color: #3b82f6;
font-size: 0.875rem;
}
.schedule-subject {
font-weight: 500;
color: #1f2937;
}
.schedule-room {
font-size: 0.875rem;
color: #6b7280;
}
/* Grades List */
.grades-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.grade-item {
display: grid;
grid-template-columns: 1fr auto;
gap: 0.5rem;
padding: 0.75rem;
background: #f9fafb;
border-radius: 0.5rem;
}
.grade-subject {
font-weight: 500;
color: #1f2937;
}
.grade-value {
font-weight: 600;
color: #22c55e;
}
.grade-eval {
font-size: 0.875rem;
color: #6b7280;
grid-column: 1 / -1;
}
/* Homework List */
.homework-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.homework-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.75rem;
background: #f9fafb;
border-radius: 0.5rem;
border-left: 3px solid #3b82f6;
}
.homework-item.done {
border-left-color: #22c55e;
opacity: 0.7;
}
.homework-subject {
font-size: 0.75rem;
font-weight: 600;
color: #3b82f6;
text-transform: uppercase;
}
.homework-title {
font-weight: 500;
color: #1f2937;
}
.homework-due {
font-size: 0.875rem;
color: #6b7280;
}
.homework-status {
font-size: 0.75rem;
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
width: fit-content;
}
.homework-status.done {
background: #dcfce7;
color: #166534;
}
.homework-status.late {
background: #fee2e2;
color: #991b1b;
}
</style>

View File

@@ -0,0 +1,330 @@
<script lang="ts">
import type { DemoData } from '$types';
import DashboardSection from '$lib/components/molecules/DashboardSection.svelte';
import SkeletonList from '$lib/components/atoms/Skeleton/SkeletonList.svelte';
let {
demoData,
isLoading = false,
hasRealData = false,
isMinor = true
}: {
demoData: DemoData;
isLoading?: boolean;
hasRealData?: boolean;
isMinor?: boolean;
} = $props();
</script>
<div class="dashboard-student">
<header class="dashboard-header">
<h1>Mon espace</h1>
<p class="dashboard-subtitle">
{#if isMinor}
Bienvenue ! Voici ton tableau de bord.
{:else}
Bienvenue ! Voici votre tableau de bord.
{/if}
</p>
</header>
{#if !hasRealData}
<div class="info-banner">
<span class="info-icon">📚</span>
<p>
{#if isMinor}
Ton emploi du temps, tes notes et tes devoirs apparaîtront ici bientôt !
{:else}
Votre emploi du temps, vos notes et vos devoirs apparaîtront ici bientôt !
{/if}
</p>
</div>
{/if}
<div class="dashboard-grid">
<!-- EDT Section -->
<DashboardSection
title="Mon emploi du temps"
subtitle={hasRealData ? "Aujourd'hui" : undefined}
isPlaceholder={!hasRealData}
placeholderMessage={isMinor ? "Ton emploi du temps sera bientôt disponible" : "Votre emploi du temps sera bientôt disponible"}
>
{#if hasRealData}
{#if isLoading}
<SkeletonList items={4} message="Chargement de l'emploi du temps..." />
{:else}
<ul class="schedule-list">
{#each demoData.schedule.today as item}
<li class="schedule-item">
<span class="schedule-time">{item.time}</span>
<span class="schedule-subject">{item.subject}</span>
<span class="schedule-room">Salle {item.room}</span>
</li>
{/each}
</ul>
{/if}
{/if}
</DashboardSection>
<!-- Notes Section -->
<DashboardSection
title="Mes notes"
subtitle={hasRealData ? "Dernières notes" : undefined}
isPlaceholder={!hasRealData}
placeholderMessage={isMinor ? "Tes notes apparaîtront ici" : "Vos notes apparaîtront ici"}
>
{#if hasRealData}
{#if isLoading}
<SkeletonList items={3} message="Chargement des notes..." />
{:else}
<ul class="grades-list">
{#each demoData.grades.recent as grade}
<li class="grade-item">
<div class="grade-header">
<span class="grade-subject">{grade.subject}</span>
<span class="grade-value">{grade.value}/{grade.max}</span>
</div>
<span class="grade-eval">{grade.evaluation}</span>
</li>
{/each}
</ul>
{/if}
{/if}
</DashboardSection>
<!-- Devoirs Section -->
<DashboardSection
title="Mes devoirs"
subtitle={hasRealData ? "À faire" : undefined}
isPlaceholder={!hasRealData}
placeholderMessage={isMinor ? "Tes devoirs s'afficheront ici" : "Vos devoirs s'afficheront ici"}
>
{#if hasRealData}
{#if isLoading}
<SkeletonList items={3} message="Chargement des devoirs..." />
{:else}
<ul class="homework-list">
{#each demoData.homework.upcoming as homework}
<li class="homework-item" class:done={homework.status === 'done'}>
<div class="homework-header">
<span class="homework-subject">{homework.subject}</span>
{#if homework.status === 'done'}
<span class="homework-badge done">Fait ✓</span>
{:else if homework.status === 'late'}
<span class="homework-badge late">En retard</span>
{/if}
</div>
<span class="homework-title">{homework.title}</span>
<span class="homework-due">Pour le {homework.dueDate}</span>
</li>
{/each}
</ul>
{/if}
{/if}
</DashboardSection>
</div>
</div>
<style>
.dashboard-student {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.dashboard-header {
margin-bottom: 0.5rem;
}
.dashboard-header h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
}
.dashboard-subtitle {
margin: 0.25rem 0 0;
color: #6b7280;
}
.info-banner {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.25rem;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 0.75rem;
color: #1e40af;
}
.info-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.info-banner p {
margin: 0;
font-size: 0.875rem;
}
.dashboard-grid {
display: grid;
gap: 1.5rem;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.dashboard-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.dashboard-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* Schedule List */
.schedule-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.schedule-item {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.75rem;
padding: 0.75rem;
background: #f9fafb;
border-radius: 0.5rem;
align-items: center;
}
.schedule-time {
font-weight: 600;
color: #3b82f6;
font-size: 0.875rem;
min-width: 3rem;
}
.schedule-subject {
font-weight: 500;
color: #1f2937;
}
.schedule-room {
font-size: 0.75rem;
color: #6b7280;
}
/* Grades List */
.grades-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.grade-item {
padding: 0.75rem;
background: #f9fafb;
border-radius: 0.5rem;
}
.grade-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
}
.grade-subject {
font-weight: 500;
color: #1f2937;
}
.grade-value {
font-weight: 700;
color: #22c55e;
font-size: 1.125rem;
}
.grade-eval {
font-size: 0.875rem;
color: #6b7280;
}
/* Homework List */
.homework-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.homework-item {
padding: 0.75rem;
background: #f9fafb;
border-radius: 0.5rem;
border-left: 3px solid #3b82f6;
}
.homework-item.done {
border-left-color: #22c55e;
opacity: 0.7;
}
.homework-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
}
.homework-subject {
font-size: 0.75rem;
font-weight: 600;
color: #3b82f6;
text-transform: uppercase;
}
.homework-badge {
font-size: 0.625rem;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-weight: 500;
}
.homework-badge.done {
background: #dcfce7;
color: #166534;
}
.homework-badge.late {
background: #fee2e2;
color: #991b1b;
}
.homework-title {
display: block;
font-weight: 500;
color: #1f2937;
margin-bottom: 0.25rem;
}
.homework-due {
font-size: 0.875rem;
color: #6b7280;
}
</style>

View File

@@ -0,0 +1,172 @@
<script lang="ts">
import DashboardSection from '$lib/components/molecules/DashboardSection.svelte';
import SkeletonList from '$lib/components/atoms/Skeleton/SkeletonList.svelte';
let {
isLoading = false,
hasRealData = false
}: {
isLoading?: boolean;
hasRealData?: boolean;
} = $props();
</script>
<div class="dashboard-teacher">
<header class="dashboard-header">
<h1>Tableau de bord Enseignant</h1>
<p class="dashboard-subtitle">Bienvenue ! Voici vos outils du jour.</p>
</header>
<div class="quick-actions">
<h2 class="sr-only">Actions rapides</h2>
<div class="action-cards">
<button type="button" class="action-card" disabled={!hasRealData}>
<span class="action-icon">📋</span>
<span class="action-label">Faire l'appel</span>
</button>
<button type="button" class="action-card" disabled={!hasRealData}>
<span class="action-icon">📝</span>
<span class="action-label">Saisir des notes</span>
</button>
<button type="button" class="action-card" disabled={!hasRealData}>
<span class="action-icon">📚</span>
<span class="action-label">Créer un devoir</span>
</button>
</div>
</div>
<div class="dashboard-grid">
<DashboardSection
title="Mes classes aujourd'hui"
isPlaceholder={!hasRealData}
placeholderMessage="Vos classes apparaîtront ici une fois l'emploi du temps configuré"
>
{#if hasRealData && isLoading}
<SkeletonList items={4} message="Chargement des classes..." />
{/if}
</DashboardSection>
<DashboardSection
title="Notes à saisir"
subtitle="Évaluations en attente"
isPlaceholder={!hasRealData}
placeholderMessage="Les évaluations en attente de notation seront listées ici"
>
{#if hasRealData && isLoading}
<SkeletonList items={3} message="Chargement des évaluations..." />
{/if}
</DashboardSection>
<DashboardSection
title="Appels du jour"
isPlaceholder={!hasRealData}
placeholderMessage="Les appels à effectuer s'afficheront ici"
>
{#if hasRealData && isLoading}
<SkeletonList items={3} message="Chargement des appels..." />
{/if}
</DashboardSection>
<DashboardSection
title="Statistiques rapides"
isPlaceholder={!hasRealData}
placeholderMessage="Les statistiques de vos classes seront disponibles prochainement"
>
{#if hasRealData && isLoading}
<SkeletonList items={2} message="Chargement des statistiques..." />
{/if}
</DashboardSection>
</div>
</div>
<style>
.dashboard-teacher {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.dashboard-header {
margin-bottom: 0.5rem;
}
.dashboard-header h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
}
.dashboard-subtitle {
margin: 0.25rem 0 0;
color: #6b7280;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.quick-actions {
margin-bottom: 0.5rem;
}
.action-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 1rem;
}
.action-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1.25rem 1rem;
background: white;
border: 2px solid #e5e7eb;
border-radius: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.action-card:hover:not(:disabled) {
border-color: #3b82f6;
background: #eff6ff;
}
.action-card:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-icon {
font-size: 1.75rem;
}
.action-label {
font-weight: 500;
color: #374151;
text-align: center;
font-size: 0.875rem;
}
.dashboard-grid {
display: grid;
gap: 1.5rem;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.dashboard-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>