L'élève avait accès à ses compétences mais pas à ses notes numériques. Cette fonctionnalité lui donne une vue complète de sa progression scolaire avec moyennes par matière, détail par évaluation, statistiques de classe, et un mode "découverte" pour révéler ses notes à son rythme (FR14, FR15). Les notes ne sont visibles qu'après publication par l'enseignant, ce qui garantit que l'élève les découvre avant ses parents (délai 24h story 6.7).
932 lines
22 KiB
Svelte
932 lines
22 KiB
Svelte
<script lang="ts">
|
|
import type { DemoData } from '$types';
|
|
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
|
|
import type { StudentHomework, StudentHomeworkDetail } from '$lib/features/homework/api/studentHomework';
|
|
import type { StudentGrade } from '$lib/features/grades/api/studentGrades';
|
|
import { fetchDaySchedule, fetchNextClass } from '$lib/features/schedule/api/schedule';
|
|
import { fetchStudentHomework, fetchHomeworkDetail } from '$lib/features/homework/api/studentHomework';
|
|
import type { StudentAverages } from '$lib/features/grades/api/studentGrades';
|
|
import { fetchMyGrades, fetchMyAverages } from '$lib/features/grades/api/studentGrades';
|
|
import {
|
|
isGradeNew,
|
|
markGradesSeen,
|
|
isDiscoverMode,
|
|
setRevealMode,
|
|
isGradeRevealed,
|
|
revealGrade
|
|
} from '$lib/features/grades/stores/gradePreferences.svelte';
|
|
import HomeworkDetail from '$lib/components/organisms/StudentHomework/HomeworkDetail.svelte';
|
|
import { recordSync } from '$lib/features/schedule/stores/scheduleCache.svelte';
|
|
import { getHomeworkStatuses } from '$lib/features/homework/stores/homeworkStatus.svelte';
|
|
import DashboardSection from '$lib/components/molecules/DashboardSection.svelte';
|
|
import SkeletonList from '$lib/components/atoms/Skeleton/SkeletonList.svelte';
|
|
import ScheduleWidget from '$lib/components/organisms/StudentSchedule/ScheduleWidget.svelte';
|
|
import { getActiveRole } from '$features/roles/roleContext.svelte';
|
|
|
|
let {
|
|
demoData,
|
|
isLoading = false,
|
|
hasRealData = false,
|
|
isMinor = true
|
|
}: {
|
|
demoData: DemoData;
|
|
isLoading?: boolean;
|
|
hasRealData?: boolean;
|
|
isMinor?: boolean;
|
|
} = $props();
|
|
|
|
let isEleve = $derived(getActiveRole() === 'ROLE_ELEVE');
|
|
|
|
// Schedule widget state (AC1: "0 tap" — visible dès le dashboard)
|
|
let scheduleSlots = $state<ScheduleSlot[]>([]);
|
|
let scheduleNextSlotId = $state<string | null>(null);
|
|
let scheduleLoading = $state(false);
|
|
let scheduleError = $state<string | null>(null);
|
|
|
|
// Homework widget state
|
|
let studentHomeworks = $state<StudentHomework[]>([]);
|
|
let homeworkLoading = $state(false);
|
|
|
|
// Grades widget state
|
|
let recentGrades = $state<StudentGrade[]>([]);
|
|
let studentAverages = $state<StudentAverages | null>(null);
|
|
let gradesLoading = $state(false);
|
|
|
|
let hwStatuses = $derived(getHomeworkStatuses());
|
|
|
|
let pendingHomeworks = $derived(
|
|
studentHomeworks.filter(hw => !hwStatuses[hw.id]?.done).slice(0, 5)
|
|
);
|
|
|
|
function formatLocalDate(d: Date): string {
|
|
const y = d.getFullYear();
|
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
return `${y}-${m}-${day}`;
|
|
}
|
|
|
|
function formatShortDate(dateStr: string): string {
|
|
const date = new Date(dateStr + 'T00:00:00');
|
|
return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
|
|
}
|
|
|
|
async function loadTodaySchedule() {
|
|
scheduleLoading = true;
|
|
scheduleError = null;
|
|
|
|
try {
|
|
const today = formatLocalDate(new Date());
|
|
scheduleSlots = await fetchDaySchedule(today);
|
|
recordSync();
|
|
|
|
try {
|
|
const next = await fetchNextClass();
|
|
scheduleNextSlotId = next?.slotId ?? null;
|
|
} catch {
|
|
scheduleNextSlotId = null;
|
|
}
|
|
} catch (e) {
|
|
scheduleError = e instanceof Error ? e.message : 'Erreur de chargement';
|
|
} finally {
|
|
scheduleLoading = false;
|
|
}
|
|
}
|
|
|
|
async function loadHomeworks() {
|
|
homeworkLoading = true;
|
|
|
|
try {
|
|
studentHomeworks = await fetchStudentHomework();
|
|
} catch {
|
|
// Silently fail on dashboard widget
|
|
} finally {
|
|
homeworkLoading = false;
|
|
}
|
|
}
|
|
|
|
let gradeSeenTimerId: number | null = null;
|
|
|
|
async function loadGrades() {
|
|
gradesLoading = true;
|
|
|
|
try {
|
|
const [all, avgs] = await Promise.all([fetchMyGrades(), fetchMyAverages()]);
|
|
recentGrades = all.slice(0, 5);
|
|
studentAverages = avgs;
|
|
|
|
const ids = all.map((g) => g.id);
|
|
gradeSeenTimerId = window.setTimeout(() => markGradesSeen(ids), 3000);
|
|
} catch {
|
|
// Silently fail on dashboard widget
|
|
} finally {
|
|
gradesLoading = false;
|
|
}
|
|
}
|
|
|
|
function gradeColor(value: number | null, scale: number): string {
|
|
if (value === null || scale <= 0) return '#6b7280';
|
|
const normalized = (value / scale) * 20;
|
|
if (normalized >= 14) return '#22c55e';
|
|
if (normalized >= 10) return '#f59e0b';
|
|
return '#ef4444';
|
|
}
|
|
|
|
// Homework detail modal
|
|
let selectedHomeworkDetail = $state<StudentHomeworkDetail | null>(null);
|
|
|
|
async function openHomeworkDetail(homeworkId: string) {
|
|
try {
|
|
selectedHomeworkDetail = await fetchHomeworkDetail(homeworkId);
|
|
} catch {
|
|
// Fallback: navigate to full page
|
|
window.location.href = '/dashboard/homework';
|
|
}
|
|
}
|
|
|
|
function closeHomeworkDetail() {
|
|
selectedHomeworkDetail = null;
|
|
}
|
|
|
|
function handleOverlayClick(e: MouseEvent) {
|
|
if (e.target === e.currentTarget) {
|
|
closeHomeworkDetail();
|
|
closeGradeDetail();
|
|
}
|
|
}
|
|
|
|
function handleKeydown(e: KeyboardEvent) {
|
|
if (e.key === 'Escape') {
|
|
closeHomeworkDetail();
|
|
closeGradeDetail();
|
|
}
|
|
}
|
|
|
|
// Grade detail modal
|
|
let selectedGrade = $state<StudentGrade | null>(null);
|
|
|
|
function openGradeDetail(grade: StudentGrade) {
|
|
selectedGrade = grade;
|
|
}
|
|
|
|
function closeGradeDetail() {
|
|
selectedGrade = null;
|
|
}
|
|
|
|
function toggleDiscoverMode() {
|
|
setRevealMode(isDiscoverMode() ? 'immediate' : 'discover');
|
|
}
|
|
|
|
function handleReveal(gradeId: string) {
|
|
revealGrade(gradeId);
|
|
}
|
|
|
|
function formatDate(dateStr: string): string {
|
|
const d = new Date(dateStr + 'T00:00:00');
|
|
return d.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
|
|
}
|
|
|
|
$effect(() => {
|
|
if (!isEleve) return;
|
|
void loadTodaySchedule();
|
|
void loadHomeworks();
|
|
void loadGrades();
|
|
return () => {
|
|
if (gradeSeenTimerId !== null) window.clearTimeout(gradeSeenTimerId);
|
|
};
|
|
});
|
|
</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={isEleve ? "Aujourd'hui" : (hasRealData ? "Aujourd'hui" : undefined)}
|
|
isPlaceholder={!isEleve && !hasRealData}
|
|
placeholderMessage={isMinor ? "Ton emploi du temps sera bientôt disponible" : "Votre emploi du temps sera bientôt disponible"}
|
|
>
|
|
{#if isEleve}
|
|
<ScheduleWidget
|
|
slots={scheduleSlots}
|
|
nextSlotId={scheduleNextSlotId}
|
|
isLoading={scheduleLoading}
|
|
error={scheduleError}
|
|
/>
|
|
{:else 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={isEleve ? "Dernières notes" : (hasRealData ? "Dernières notes" : undefined)}
|
|
isPlaceholder={!isEleve && !hasRealData}
|
|
placeholderMessage={isMinor ? "Tes notes apparaîtront ici" : "Vos notes apparaîtront ici"}
|
|
>
|
|
{#if isEleve}
|
|
{#if gradesLoading}
|
|
<SkeletonList items={3} message="Chargement des notes..." />
|
|
{:else if recentGrades.length === 0}
|
|
<p class="empty-grades">Aucune note publiée</p>
|
|
{:else}
|
|
<div class="widget-grades-header">
|
|
{#if studentAverages?.generalAverage != null}
|
|
<div class="widget-general-avg">
|
|
<span class="widget-avg-label">Moyenne générale</span>
|
|
<span class="widget-avg-value" style:color={gradeColor(studentAverages.generalAverage, 20)}>
|
|
{studentAverages.generalAverage.toFixed(1)}/20
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
<label class="widget-discover-toggle">
|
|
<input type="checkbox" checked={isDiscoverMode()} onchange={toggleDiscoverMode} />
|
|
<span>Mode découverte</span>
|
|
</label>
|
|
</div>
|
|
<ul class="grades-list">
|
|
{#each recentGrades as grade}
|
|
{@const discover = isDiscoverMode() && !isGradeRevealed(grade.id)}
|
|
<li>
|
|
<button class="grade-item grade-item-btn" onclick={() => discover ? handleReveal(grade.id) : openGradeDetail(grade)}>
|
|
<div class="grade-header">
|
|
<span class="grade-subject">{grade.subjectName ?? 'Matière'}</span>
|
|
{#if isGradeNew(grade.id)}
|
|
<span class="grade-badge-new">Nouveau</span>
|
|
{/if}
|
|
{#if discover}
|
|
<span class="grade-value grade-blur">??/{grade.gradeScale}</span>
|
|
{:else if grade.status === 'graded' && grade.value != null}
|
|
<span class="grade-value" style:color={gradeColor(grade.value, grade.gradeScale)}>
|
|
{grade.value}/{grade.gradeScale}
|
|
</span>
|
|
{:else if grade.status === 'absent'}
|
|
<span class="grade-value" style:color="#f59e0b">Absent</span>
|
|
{:else if grade.status === 'dispensed'}
|
|
<span class="grade-value" style:color="#6b7280">Dispensé</span>
|
|
{/if}
|
|
</div>
|
|
<span class="grade-eval">{grade.evaluationTitle}</span>
|
|
{#if discover}
|
|
<span class="grade-reveal-hint">Cliquer pour révéler</span>
|
|
{/if}
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
<a href="/dashboard/student-grades" class="view-all-link">
|
|
Voir toutes les notes →
|
|
</a>
|
|
{/if}
|
|
{:else 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={isEleve ? "À faire" : (hasRealData ? "À faire" : undefined)}
|
|
isPlaceholder={!isEleve && !hasRealData}
|
|
placeholderMessage={isMinor ? "Tes devoirs s'afficheront ici" : "Vos devoirs s'afficheront ici"}
|
|
>
|
|
{#if isEleve}
|
|
{#if homeworkLoading}
|
|
<SkeletonList items={3} message="Chargement des devoirs..." />
|
|
{:else if pendingHomeworks.length === 0}
|
|
<p class="empty-homework">Aucun devoir à faire</p>
|
|
{:else}
|
|
<ul class="homework-list">
|
|
{#each pendingHomeworks as homework}
|
|
<li>
|
|
<button class="homework-item" style:border-left-color={homework.subjectColor ?? '#3b82f6'} onclick={() => openHomeworkDetail(homework.id)}>
|
|
<div class="homework-header">
|
|
<span class="homework-subject" style:color={homework.subjectColor ?? '#3b82f6'}>{homework.subjectName}</span>
|
|
</div>
|
|
<span class="homework-title">{homework.title}</span>
|
|
<span class="homework-due">Pour le {formatShortDate(homework.dueDate)}</span>
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
<a href="/dashboard/homework" class="view-all-link">
|
|
Voir tous les devoirs →
|
|
</a>
|
|
{/if}
|
|
{:else 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>
|
|
|
|
<svelte:window onkeydown={handleKeydown} />
|
|
|
|
{#if selectedHomeworkDetail}
|
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
<div class="homework-modal-overlay" onclick={handleOverlayClick} role="presentation">
|
|
<div class="homework-modal" role="dialog" aria-modal="true" aria-label="Détail du devoir">
|
|
<button class="homework-modal-close" onclick={closeHomeworkDetail} aria-label="Fermer">×</button>
|
|
<HomeworkDetail detail={selectedHomeworkDetail} onBack={closeHomeworkDetail} />
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if selectedGrade}
|
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
<div class="homework-modal-overlay" onclick={handleOverlayClick} role="presentation">
|
|
<div class="grade-detail-modal" role="dialog" aria-modal="true" aria-label="Détail de la note">
|
|
<button class="homework-modal-close" onclick={closeGradeDetail} aria-label="Fermer">×</button>
|
|
<div class="grade-detail-header">
|
|
<span class="grade-detail-subject">{selectedGrade.subjectName ?? 'Matière'}</span>
|
|
<span class="grade-detail-date">{formatDate(selectedGrade.evaluationDate)}</span>
|
|
</div>
|
|
<h3 class="grade-detail-title">{selectedGrade.evaluationTitle}</h3>
|
|
<div class="grade-detail-score">
|
|
{#if selectedGrade.status === 'graded' && selectedGrade.value != null}
|
|
<span class="grade-detail-value" style:color={gradeColor(selectedGrade.value, selectedGrade.gradeScale)}>
|
|
{selectedGrade.value}/{selectedGrade.gradeScale}
|
|
</span>
|
|
{:else if selectedGrade.status === 'absent'}
|
|
<span class="grade-detail-value" style:color="#f59e0b">Absent</span>
|
|
{:else if selectedGrade.status === 'dispensed'}
|
|
<span class="grade-detail-value" style:color="#6b7280">Dispensé</span>
|
|
{/if}
|
|
<span class="grade-detail-coeff">Coeff. {selectedGrade.coefficient}</span>
|
|
</div>
|
|
{#if selectedGrade.appreciation}
|
|
<div class="grade-detail-appreciation">
|
|
<span class="grade-detail-label">Appréciation</span>
|
|
<p>{selectedGrade.appreciation}</p>
|
|
</div>
|
|
{/if}
|
|
{#if selectedGrade.classAverage != null}
|
|
<div class="grade-detail-stats">
|
|
<span class="grade-detail-label">Statistiques de la classe</span>
|
|
<div class="grade-detail-stats-grid">
|
|
<div class="stat-item">
|
|
<span class="stat-value">{selectedGrade.classAverage.toFixed(1)}</span>
|
|
<span class="stat-label">Moyenne</span>
|
|
</div>
|
|
{#if selectedGrade.classMin != null}
|
|
<div class="stat-item">
|
|
<span class="stat-value">{selectedGrade.classMin.toFixed(1)}</span>
|
|
<span class="stat-label">Min</span>
|
|
</div>
|
|
{/if}
|
|
{#if selectedGrade.classMax != null}
|
|
<div class="stat-item">
|
|
<span class="stat-value">{selectedGrade.classMax.toFixed(1)}</span>
|
|
<span class="stat-label">Max</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<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-item-btn {
|
|
display: block;
|
|
width: 100%;
|
|
border: none;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.grade-item-btn:hover {
|
|
background: #f3f4f6;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.grade-badge-new {
|
|
font-size: 0.5625rem;
|
|
padding: 0.0625rem 0.375rem;
|
|
background: #eff6ff;
|
|
color: #2563eb;
|
|
border-radius: 1rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.empty-grades {
|
|
margin: 0;
|
|
text-align: center;
|
|
padding: 1rem;
|
|
color: #6b7280;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.widget-grades-header {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.widget-general-avg {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.5rem 0.75rem;
|
|
background: #f0fdf4;
|
|
border: 1px solid #bbf7d0;
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
.widget-avg-label {
|
|
font-size: 0.75rem;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.widget-avg-value {
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.widget-discover-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
cursor: pointer;
|
|
font-size: 0.75rem;
|
|
color: #6b7280;
|
|
align-self: flex-end;
|
|
}
|
|
|
|
.widget-discover-toggle input {
|
|
accent-color: #8b5cf6;
|
|
}
|
|
|
|
.grade-blur {
|
|
filter: blur(4px);
|
|
color: #9ca3af !important;
|
|
}
|
|
|
|
.grade-reveal-hint {
|
|
display: block;
|
|
font-size: 0.625rem;
|
|
color: #8b5cf6;
|
|
text-align: right;
|
|
margin-top: 0.125rem;
|
|
}
|
|
|
|
/* Grade detail modal */
|
|
.grade-detail-modal {
|
|
position: relative;
|
|
background: white;
|
|
border-radius: 0.75rem;
|
|
padding: 1.5rem;
|
|
max-width: 28rem;
|
|
width: 100%;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.grade-detail-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 0.25rem;
|
|
padding-right: 2rem;
|
|
}
|
|
|
|
.grade-detail-subject {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: #3b82f6;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.grade-detail-date {
|
|
font-size: 0.75rem;
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.grade-detail-title {
|
|
margin: 0 0 1rem;
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.grade-detail-score {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.75rem;
|
|
background: #f9fafb;
|
|
border-radius: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.grade-detail-value {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.grade-detail-coeff {
|
|
font-size: 0.75rem;
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.grade-detail-appreciation {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.grade-detail-label {
|
|
display: block;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: #6b7280;
|
|
text-transform: uppercase;
|
|
margin-bottom: 0.375rem;
|
|
}
|
|
|
|
.grade-detail-appreciation p {
|
|
margin: 0;
|
|
font-size: 0.875rem;
|
|
color: #4b5563;
|
|
font-style: italic;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.grade-detail-stats {
|
|
padding: 0.75rem;
|
|
background: #f9fafb;
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
.grade-detail-stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 0.5rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.125rem;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.125rem;
|
|
font-weight: 700;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.625rem;
|
|
color: #9ca3af;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* Homework List */
|
|
.homework-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.homework-item {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
background: #f9fafb;
|
|
border: none;
|
|
border-radius: 0.5rem;
|
|
border-left: 3px solid #3b82f6;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
font: inherit;
|
|
}
|
|
|
|
button.homework-item:hover {
|
|
background: #f3f4f6;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.empty-homework {
|
|
margin: 0;
|
|
text-align: center;
|
|
padding: 1rem;
|
|
color: #6b7280;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.view-all-link {
|
|
display: block;
|
|
text-align: center;
|
|
margin-top: 0.75rem;
|
|
color: #3b82f6;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.view-all-link:hover {
|
|
color: #2563eb;
|
|
}
|
|
|
|
/* Homework detail modal */
|
|
.homework-modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 50;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.homework-modal {
|
|
position: relative;
|
|
background: white;
|
|
border-radius: 0.75rem;
|
|
padding: 1.5rem;
|
|
max-width: 40rem;
|
|
width: 100%;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.homework-modal-close {
|
|
position: absolute;
|
|
top: 0.75rem;
|
|
right: 0.75rem;
|
|
background: none;
|
|
border: none;
|
|
font-size: 1.5rem;
|
|
color: #6b7280;
|
|
cursor: pointer;
|
|
line-height: 1;
|
|
padding: 0.25rem;
|
|
}
|
|
|
|
.homework-modal-close:hover {
|
|
color: #1f2937;
|
|
}
|
|
</style>
|