feat: Permettre à l'élève de consulter ses notes et moyennes
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).
This commit is contained in:
@@ -109,11 +109,13 @@
|
||||
{/if}
|
||||
{#if isEleve}
|
||||
<a href="/dashboard/schedule" class="nav-link" class:active={pathname === '/dashboard/schedule'}>Mon EDT</a>
|
||||
<a href="/dashboard/student-grades" class="nav-link" class:active={pathname === '/dashboard/student-grades'}>Mes notes</a>
|
||||
<a href="/dashboard/student-competencies" class="nav-link" class:active={pathname === '/dashboard/student-competencies'}>Compétences</a>
|
||||
{/if}
|
||||
{#if isParent}
|
||||
<a href="/dashboard/parent-schedule" class="nav-link" class:active={pathname === '/dashboard/parent-schedule'}>EDT enfants</a>
|
||||
<a href="/dashboard/parent-homework" class="nav-link" class:active={pathname === '/dashboard/parent-homework'}>Devoirs</a>
|
||||
<a href="/dashboard/parent-grades" class="nav-link" class:active={pathname === '/dashboard/parent-grades'}>Notes</a>
|
||||
{/if}
|
||||
<button class="nav-button" onclick={goSettings}>Paramètres</button>
|
||||
<button class="logout-button" onclick={handleLogout} disabled={isLoggingOut}>
|
||||
@@ -164,6 +166,9 @@
|
||||
<a href="/dashboard/schedule" class="mobile-nav-link" class:active={pathname === '/dashboard/schedule'}>
|
||||
Mon emploi du temps
|
||||
</a>
|
||||
<a href="/dashboard/student-grades" class="mobile-nav-link" class:active={pathname === '/dashboard/student-grades'}>
|
||||
Mes notes
|
||||
</a>
|
||||
<a href="/dashboard/student-competencies" class="mobile-nav-link" class:active={pathname === '/dashboard/student-competencies'}>
|
||||
Compétences
|
||||
</a>
|
||||
@@ -175,6 +180,9 @@
|
||||
<a href="/dashboard/parent-homework" class="mobile-nav-link" class:active={pathname === '/dashboard/parent-homework'}>
|
||||
Devoirs
|
||||
</a>
|
||||
<a href="/dashboard/parent-grades" class="mobile-nav-link" class:active={pathname === '/dashboard/parent-grades'}>
|
||||
Notes
|
||||
</a>
|
||||
{/if}
|
||||
<button class="mobile-nav-link" onclick={goSettings}>Paramètres</button>
|
||||
</div>
|
||||
|
||||
29
frontend/src/routes/dashboard/parent-grades/+page.svelte
Normal file
29
frontend/src/routes/dashboard/parent-grades/+page.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import ParentGradesView from '$lib/components/organisms/ParentGrades/ParentGradesView.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Notes des enfants - Classeo</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="grades-page">
|
||||
<header class="page-header">
|
||||
<h1>Notes des enfants</h1>
|
||||
</header>
|
||||
<ParentGradesView />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.grades-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
</style>
|
||||
695
frontend/src/routes/dashboard/student-grades/+page.svelte
Normal file
695
frontend/src/routes/dashboard/student-grades/+page.svelte
Normal file
@@ -0,0 +1,695 @@
|
||||
<script lang="ts">
|
||||
import type { StudentGrade, StudentAverages, SubjectAverage } from '$lib/features/grades/api/studentGrades';
|
||||
import { fetchMyGrades, fetchMyAverages } from '$lib/features/grades/api/studentGrades';
|
||||
import {
|
||||
setRevealMode,
|
||||
isGradeNew,
|
||||
markGradesSeen,
|
||||
isGradeRevealed,
|
||||
revealGrade,
|
||||
isDiscoverMode
|
||||
} from '$lib/features/grades/stores/gradePreferences.svelte';
|
||||
import SkeletonList from '$lib/components/atoms/Skeleton/SkeletonList.svelte';
|
||||
|
||||
let grades: StudentGrade[] = $state([]);
|
||||
let averages: StudentAverages | null = $state(null);
|
||||
let isLoading = $state(true);
|
||||
let error: string | null = $state(null);
|
||||
let selectedSubjectId: string | null = $state(null);
|
||||
|
||||
// Group grades by subject
|
||||
let subjectGroups = $derived.by(() => {
|
||||
const map = new Map<string, { subjectName: string; grades: StudentGrade[] }>();
|
||||
for (const g of grades) {
|
||||
const key = g.subjectId;
|
||||
const existing = map.get(key);
|
||||
if (existing) {
|
||||
existing.grades.push(g);
|
||||
} else {
|
||||
map.set(key, { subjectName: g.subjectName ?? 'Matière inconnue', grades: [g] });
|
||||
}
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
// Find average for a subject
|
||||
function subjectAverage(subjectId: string): SubjectAverage | undefined {
|
||||
return averages?.subjectAverages.find((a) => a.subjectId === subjectId);
|
||||
}
|
||||
|
||||
// Filtered grades for selected subject detail
|
||||
let detailGrades = $derived.by(() => {
|
||||
if (!selectedSubjectId) return [];
|
||||
return grades.filter((g) => g.subjectId === selectedSubjectId);
|
||||
});
|
||||
|
||||
let detailSubjectName = $derived.by(() => {
|
||||
if (!selectedSubjectId) return '';
|
||||
return subjectGroups.get(selectedSubjectId)?.subjectName ?? '';
|
||||
});
|
||||
|
||||
let detailAverage = $derived.by(() => {
|
||||
if (!selectedSubjectId) return undefined;
|
||||
return subjectAverage(selectedSubjectId);
|
||||
});
|
||||
|
||||
// Color based on grade value (green ≥ 14, orange 10-14, red < 10, on /20 scale)
|
||||
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';
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const d = new Date(dateStr + 'T00:00:00');
|
||||
return d.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
}
|
||||
|
||||
function handleReveal(gradeId: string) {
|
||||
revealGrade(gradeId);
|
||||
}
|
||||
|
||||
function toggleDiscoverMode() {
|
||||
const newMode = isDiscoverMode() ? 'immediate' : 'discover';
|
||||
setRevealMode(newMode);
|
||||
}
|
||||
|
||||
let seenTimerId: number | null = null;
|
||||
|
||||
$effect(() => {
|
||||
loadData();
|
||||
return () => {
|
||||
if (seenTimerId !== null) window.clearTimeout(seenTimerId);
|
||||
};
|
||||
});
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
|
||||
const [gradesData, averagesData] = await Promise.all([fetchMyGrades(), fetchMyAverages()]);
|
||||
|
||||
grades = gradesData;
|
||||
averages = averagesData;
|
||||
|
||||
// Mark all loaded grades as seen (for "Nouveau" badge)
|
||||
const ids = gradesData.map((g) => g.id);
|
||||
// Delay marking to let the badge show briefly
|
||||
seenTimerId = window.setTimeout(() => markGradesSeen(ids), 3000);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Erreur inconnue';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openSubjectDetail(subjectId: string) {
|
||||
selectedSubjectId = subjectId;
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
selectedSubjectId = null;
|
||||
}
|
||||
|
||||
function handleOverlayClick(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) closeDetail();
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && selectedSubjectId) closeDetail();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="student-grades">
|
||||
<header class="page-header">
|
||||
<div class="header-top">
|
||||
<h1>Mes notes</h1>
|
||||
<label class="discover-toggle">
|
||||
<input type="checkbox" checked={isDiscoverMode()} onchange={toggleDiscoverMode} />
|
||||
<span class="toggle-label">Mode découverte</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if averages?.generalAverage != null}
|
||||
<div class="general-average">
|
||||
<span class="avg-label">Moyenne générale</span>
|
||||
<span class="avg-value" style:color={gradeColor(averages.generalAverage, 20)}>
|
||||
{averages.generalAverage.toFixed(1)}/20
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if error}
|
||||
<div class="error-banner" role="alert">
|
||||
<p>{error}</p>
|
||||
<button onclick={() => (error = null)}>Fermer</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isLoading}
|
||||
<SkeletonList items={5} message="Chargement des notes..." />
|
||||
{:else if grades.length === 0}
|
||||
<div class="empty-state">
|
||||
<p>Aucune note publiée pour le moment.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Subject averages summary -->
|
||||
{#if averages && averages.subjectAverages.length > 0}
|
||||
<section class="averages-section">
|
||||
<h2>Moyennes par matière</h2>
|
||||
<div class="averages-grid">
|
||||
{#each averages.subjectAverages as avg}
|
||||
<button
|
||||
class="average-card"
|
||||
onclick={() => openSubjectDetail(avg.subjectId)}
|
||||
>
|
||||
<span class="avg-subject">{avg.subjectName ?? 'Matière'}</span>
|
||||
<span class="avg-score" style:color={gradeColor(avg.average, 20)}>
|
||||
{avg.average.toFixed(1)}
|
||||
</span>
|
||||
<span class="avg-count">{avg.gradeCount} note{avg.gradeCount > 1 ? 's' : ''}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Recent grades -->
|
||||
<section class="recent-section">
|
||||
<h2>Dernières notes</h2>
|
||||
<ul class="grades-list">
|
||||
{#each grades as grade (grade.id)}
|
||||
{@const discover = isDiscoverMode() && !isGradeRevealed(grade.id)}
|
||||
{@const isNew = isGradeNew(grade.id)}
|
||||
<li>
|
||||
<button class="grade-card grade-card-btn" onclick={() => discover ? handleReveal(grade.id) : openSubjectDetail(grade.subjectId)}>
|
||||
<div class="grade-card-header">
|
||||
<span class="grade-subject">{grade.subjectName ?? 'Matière'}</span>
|
||||
<span class="grade-date">{formatDate(grade.evaluationDate)}</span>
|
||||
{#if isNew}
|
||||
<span class="badge-new">Nouveau</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grade-card-body">
|
||||
<span class="grade-eval-title">{grade.evaluationTitle}</span>
|
||||
{#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-status absent">Absent</span>
|
||||
{:else if grade.status === 'dispensed'}
|
||||
<span class="grade-status dispensed">Dispensé</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !discover && grade.classAverage != null}
|
||||
<div class="grade-card-stats">
|
||||
<span>Moy. classe : {grade.classAverage.toFixed(1)}</span>
|
||||
{#if grade.classMin != null && grade.classMax != null}
|
||||
<span>Min : {grade.classMin.toFixed(1)} / Max : {grade.classMax.toFixed(1)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !discover && grade.appreciation}
|
||||
<p class="grade-appreciation">{grade.appreciation}</p>
|
||||
{/if}
|
||||
<div class="grade-card-meta">
|
||||
<span class="grade-coeff">Coeff. {grade.coefficient}</span>
|
||||
</div>
|
||||
{#if discover}
|
||||
<span class="reveal-hint">Cliquer pour révéler</span>
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Subject detail modal -->
|
||||
{#if selectedSubjectId}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div class="modal-overlay" onclick={handleOverlayClick} role="presentation">
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-label="Détail matière {detailSubjectName}">
|
||||
<div class="modal-header">
|
||||
<h2>{detailSubjectName}</h2>
|
||||
<button class="modal-close" onclick={closeDetail} aria-label="Fermer">×</button>
|
||||
</div>
|
||||
{#if detailAverage}
|
||||
<div class="modal-average">
|
||||
<span class="avg-label">Moyenne</span>
|
||||
<span class="avg-value" style:color={gradeColor(detailAverage.average, 20)}>
|
||||
{detailAverage.average.toFixed(1)}/20
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<ul class="detail-list">
|
||||
{#each detailGrades as grade (grade.id)}
|
||||
{@const discover = isDiscoverMode() && !isGradeRevealed(grade.id)}
|
||||
<li class="detail-item">
|
||||
<div class="detail-header">
|
||||
<span class="detail-title">{grade.evaluationTitle}</span>
|
||||
<span class="detail-date">{formatDate(grade.evaluationDate)}</span>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
{#if discover}
|
||||
<button class="detail-reveal-btn" onclick={() => handleReveal(grade.id)}>
|
||||
<span class="grade-blur">??/{grade.gradeScale}</span>
|
||||
<span class="reveal-hint">Révéler</span>
|
||||
</button>
|
||||
{: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-status absent">Absent</span>
|
||||
{:else if grade.status === 'dispensed'}
|
||||
<span class="grade-status dispensed">Dispensé</span>
|
||||
{/if}
|
||||
<span class="grade-coeff">Coeff. {grade.coefficient}</span>
|
||||
</div>
|
||||
{#if !discover && grade.classAverage != null}
|
||||
<div class="grade-card-stats">
|
||||
<span>Moy. classe : {grade.classAverage.toFixed(1)}</span>
|
||||
{#if grade.classMin != null && grade.classMax != null}
|
||||
<span>Min : {grade.classMin.toFixed(1)} / Max : {grade.classMax.toFixed(1)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !discover && grade.appreciation}
|
||||
<p class="grade-appreciation">{grade.appreciation}</p>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.student-grades {
|
||||
max-width: 64rem;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.discover-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.discover-toggle input {
|
||||
accent-color: #8b5cf6;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.general-average {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.avg-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.avg-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 0.5rem;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.error-banner p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-banner button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #991b1b;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Averages section */
|
||||
.averages-section h2,
|
||||
.recent-section h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.averages-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
}
|
||||
|
||||
.average-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
box-shadow 0.15s,
|
||||
border-color 0.15s;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.average-card:hover {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.avg-subject {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.avg-score {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.avg-count {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Grade cards */
|
||||
.grades-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.grade-card {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.grade-card-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.grade-card-btn:hover {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.grade-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.grade-subject {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #3b82f6;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.grade-date {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.badge-new {
|
||||
font-size: 0.625rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
border-radius: 1rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.grade-card-body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.grade-eval-title {
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.grade-value {
|
||||
font-weight: 700;
|
||||
font-size: 1.125rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.grade-status {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.grade-status.absent {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.grade-status.dispensed {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.grade-card-stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.grade-appreciation {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.875rem;
|
||||
color: #4b5563;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.grade-card-meta {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.grade-coeff {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.grade-blur {
|
||||
filter: blur(4px);
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
.detail-reveal-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
background: #f3f4f6;
|
||||
border: 1px dashed #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.detail-reveal-btn:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.reveal-hint {
|
||||
display: block;
|
||||
font-size: 0.625rem;
|
||||
color: #8b5cf6;
|
||||
text-align: right;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.modal-average {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.detail-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
padding: 0.75rem;
|
||||
background: #f9fafb;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.detail-date {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user