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,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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
102
frontend/src/lib/data/demo-data.json
Normal file
102
frontend/src/lib/data/demo-data.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"serenityScore": {
|
||||
"value": 86,
|
||||
"emoji": "💚",
|
||||
"trend": "stable",
|
||||
"components": {
|
||||
"notes": {
|
||||
"score": 90,
|
||||
"label": "Excellentes notes"
|
||||
},
|
||||
"absences": {
|
||||
"score": 95,
|
||||
"label": "Très peu d'absences"
|
||||
},
|
||||
"devoirs": {
|
||||
"score": 70,
|
||||
"label": "Quelques devoirs en retard"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schedule": {
|
||||
"today": [
|
||||
{
|
||||
"time": "08:00",
|
||||
"subject": "Mathématiques",
|
||||
"room": "A101"
|
||||
},
|
||||
{
|
||||
"time": "09:00",
|
||||
"subject": "Français",
|
||||
"room": "B203"
|
||||
},
|
||||
{
|
||||
"time": "10:15",
|
||||
"subject": "Histoire-Géographie",
|
||||
"room": "C105"
|
||||
},
|
||||
{
|
||||
"time": "11:15",
|
||||
"subject": "Anglais",
|
||||
"room": "D201"
|
||||
},
|
||||
{
|
||||
"time": "14:00",
|
||||
"subject": "Sciences Physiques",
|
||||
"room": "Labo 1"
|
||||
},
|
||||
{
|
||||
"time": "15:00",
|
||||
"subject": "Éducation Physique",
|
||||
"room": "Gymnase"
|
||||
}
|
||||
]
|
||||
},
|
||||
"grades": {
|
||||
"recent": [
|
||||
{
|
||||
"subject": "Mathématiques",
|
||||
"value": 16,
|
||||
"max": 20,
|
||||
"date": "2026-01-28",
|
||||
"evaluation": "Contrôle équations"
|
||||
},
|
||||
{
|
||||
"subject": "Français",
|
||||
"value": 14,
|
||||
"max": 20,
|
||||
"date": "2026-01-25",
|
||||
"evaluation": "Dissertation"
|
||||
},
|
||||
{
|
||||
"subject": "Anglais",
|
||||
"value": 17,
|
||||
"max": 20,
|
||||
"date": "2026-01-22",
|
||||
"evaluation": "Compréhension orale"
|
||||
}
|
||||
]
|
||||
},
|
||||
"homework": {
|
||||
"upcoming": [
|
||||
{
|
||||
"subject": "Mathématiques",
|
||||
"title": "Exercices chapitre 5",
|
||||
"dueDate": "2026-02-06",
|
||||
"status": "pending"
|
||||
},
|
||||
{
|
||||
"subject": "Français",
|
||||
"title": "Lecture Le Rouge et le Noir (chap. 1-5)",
|
||||
"dueDate": "2026-02-08",
|
||||
"status": "pending"
|
||||
},
|
||||
{
|
||||
"subject": "Histoire-Géographie",
|
||||
"title": "Fiche révision WWI",
|
||||
"dueDate": "2026-02-05",
|
||||
"status": "done"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
59
frontend/src/lib/features/dashboard/serenity-score.ts
Normal file
59
frontend/src/lib/features/dashboard/serenity-score.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { SerenityScore } from '$types';
|
||||
|
||||
export type SerenityEmoji = '💚' | '🟡' | '🔴';
|
||||
|
||||
/**
|
||||
* Calculates the serenity score from its components
|
||||
* Score = (Notes × 0.4) + (Absences × 0.3) + (Devoirs × 0.3)
|
||||
*/
|
||||
export function calculateSerenityScore(components: {
|
||||
notes: number;
|
||||
absences: number;
|
||||
devoirs: number;
|
||||
}): number {
|
||||
const score = components.notes * 0.4 + components.absences * 0.3 + components.devoirs * 0.3;
|
||||
return Math.round(score);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate emoji for a serenity score
|
||||
* 💚 Green: Score >= 70
|
||||
* 🟡 Orange: Score 40-69
|
||||
* 🔴 Red: Score < 40
|
||||
*/
|
||||
export function getSerenityEmoji(score: number): SerenityEmoji {
|
||||
if (score >= 70) return '💚';
|
||||
if (score >= 40) return '🟡';
|
||||
return '🔴';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a human-readable label for the serenity score
|
||||
*/
|
||||
export function getSerenityLabel(score: number): string {
|
||||
if (score >= 70) return 'Excellent';
|
||||
if (score >= 40) return 'A surveiller';
|
||||
return 'Attention requise';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a serenity score value is within valid bounds
|
||||
*/
|
||||
export function isValidScore(score: number): boolean {
|
||||
return score >= 0 && score <= 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets trend icon for score comparison
|
||||
*/
|
||||
export function getTrendIcon(trend: SerenityScore['trend']): string {
|
||||
switch (trend) {
|
||||
case 'up':
|
||||
return '↑';
|
||||
case 'down':
|
||||
return '↓';
|
||||
case 'stable':
|
||||
default:
|
||||
return '→';
|
||||
}
|
||||
}
|
||||
55
frontend/src/lib/types/demo.ts
Normal file
55
frontend/src/lib/types/demo.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export interface SerenityComponent {
|
||||
score: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SerenityScore {
|
||||
value: number;
|
||||
emoji: '💚' | '🟡' | '🔴';
|
||||
trend: 'stable' | 'up' | 'down';
|
||||
components: {
|
||||
notes: SerenityComponent;
|
||||
absences: SerenityComponent;
|
||||
devoirs: SerenityComponent;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ScheduleItem {
|
||||
time: string;
|
||||
subject: string;
|
||||
room: string;
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
today: ScheduleItem[];
|
||||
}
|
||||
|
||||
export interface GradeItem {
|
||||
subject: string;
|
||||
value: number;
|
||||
max: number;
|
||||
date: string;
|
||||
evaluation: string;
|
||||
}
|
||||
|
||||
export interface Grades {
|
||||
recent: GradeItem[];
|
||||
}
|
||||
|
||||
export interface HomeworkItem {
|
||||
subject: string;
|
||||
title: string;
|
||||
dueDate: string;
|
||||
status: 'pending' | 'done' | 'late';
|
||||
}
|
||||
|
||||
export interface Homework {
|
||||
upcoming: HomeworkItem[];
|
||||
}
|
||||
|
||||
export interface DemoData {
|
||||
serenityScore: SerenityScore;
|
||||
schedule: Schedule;
|
||||
grades: Grades;
|
||||
homework: Homework;
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './shared';
|
||||
export * from './api';
|
||||
export * from './demo';
|
||||
|
||||
@@ -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>
|
||||
|
||||
208
frontend/src/routes/dashboard/+layout.svelte
Normal file
208
frontend/src/routes/dashboard/+layout.svelte
Normal 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>
|
||||
118
frontend/src/routes/dashboard/+page.svelte
Normal file
118
frontend/src/routes/dashboard/+page.svelte
Normal 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>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user