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:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user