CompetenceTrack v9 - Rich text editor, Drawing integration, PDF export, Notebook sharing, Templates, Keyboard shortcuts

Features:
- Rich text toolbar for notebook pages (Bold, Italic, Underline, Headings, Lists)
- Drawing canvas integration in notebook pages (toggle drawing mode)
- Notebook PDF export (client-side print with print CSS)
- Page background rendering (lined, grid, blank, dotted, music, calligraphy)
- Page thumbnails in sidebar with bookmark indicators
- Auto-save with 3-second debounce
- Notebook sharing with classes (isPublic toggle)
- Notebook duplication (copy with all pages)
- 6 pre-built notebook templates (Math, Deutsch, Englisch, Kunst, Musik, Sachkunde)
- Shared notebooks tab (view other teachers' shared notebooks)
- Templates tab with one-click creation
- Keyboard shortcuts (Cmd+N, Cmd+D, Cmd+Shift+N, Cmd+E, Cmd+Shift+A, Cmd+.)
- Student notebook access (role-based UI)
- Role indicator in notebook header (Teacher/Student)
- Onboarding tour expanded to 14 steps
- Dashboard improvements (Today's Schedule, Paper Saved counter, Recent Activity)
- Classes view improvements (student/subject counts, type badges)
- Competence flower improvements (shadow, shimmer, mastery summary)
- Progress entries improvements (mastery distribution chart)
- Global CSS improvements (dark scrollbar, focus-visible, card-hover-glow)
- 60+ new i18n keys (German/English)

Bug Fixes:
- Notebook creation not refreshing list (fixed by calling loadData after creation)
- Missing BarChart3 import in progress-entries-view
- Escaped quotes in JSX className attributes
This commit is contained in:
Mustafa Cetin committed 2026-07-29 08:02:04 +00:00
1 parent 04034dca62
commit 62f4a150e0
14 files changed
+1983 -166

No files matched your search

+61 -7
View File
@@ -279,7 +279,7 @@ export default function AppLayout() {
const isMod = e.metaKey || e.ctrlKey;
// Ctrl/Cmd + K: Open command palette
if (isMod && e.key.toLowerCase() === 'k') {
if (isMod && !e.shiftKey && e.key.toLowerCase() === 'k') {
e.preventDefault();
setSearchOpen((v) => !v);
return;
@@ -292,13 +292,67 @@ export default function AppLayout() {
return;
}
// Ctrl/Cmd + N: Create new entry (navigate to progress)
if (isMod && e.key.toLowerCase() === 'n') {
// Ctrl/Cmd + Shift + N: Add new page to current notebook (when on notebooks view)
if (isMod && e.shiftKey && e.key.toLowerCase() === 'n') {
e.preventDefault();
if (currentView === 'notebooks') {
window.dispatchEvent(new CustomEvent('ct-shortcut', { detail: 'new-page' }));
return;
}
}
// Ctrl/Cmd + Shift + A: Toggle archive mode (when on notebooks view)
if (isMod && e.shiftKey && e.key.toLowerCase() === 'a') {
e.preventDefault();
if (currentView === 'notebooks') {
window.dispatchEvent(new CustomEvent('ct-shortcut', { detail: 'archive-toggle' }));
return;
}
}
// Ctrl/Cmd + N: Context-dependent new action
if (isMod && !e.shiftKey && e.key.toLowerCase() === 'n') {
e.preventDefault();
setSearchOpen(false);
setCurrentView('progress');
trackAction('new-entry', t('shortcuts.new_entry'), 'progress');
toast.info(t('shortcuts.new_entry'));
if (currentView === 'notebooks') {
// Open create notebook dialog
window.dispatchEvent(new CustomEvent('ct-shortcut', { detail: 'new-notebook' }));
} else {
// Default: navigate to progress
setCurrentView('progress');
trackAction('new-entry', t('shortcuts.new_entry'), 'progress');
toast.info(t('shortcuts.new_entry'));
}
return;
}
// Ctrl/Cmd + D: Open drawing canvas
if (isMod && e.key.toLowerCase() === 'd') {
e.preventDefault();
if (currentView === 'notebooks') {
window.dispatchEvent(new CustomEvent('ct-shortcut', { detail: 'drawing' }));
} else {
setCurrentView('drawing');
trackAction('drawing', t('shortcuts.drawing'), 'drawing');
}
return;
}
// Ctrl/Cmd + E: Export current notebook as PDF
if (isMod && e.key.toLowerCase() === 'e') {
e.preventDefault();
if (currentView === 'notebooks') {
window.dispatchEvent(new CustomEvent('ct-shortcut', { detail: 'export-pdf' }));
}
return;
}
// Ctrl/Cmd + .: Close current notebook (go back to library)
if (isMod && e.key === '.') {
e.preventDefault();
if (currentView === 'notebooks') {
window.dispatchEvent(new CustomEvent('ct-shortcut', { detail: 'close-notebook' }));
}
return;
}
@@ -326,7 +380,7 @@ export default function AppLayout() {
}
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [searchOpen, shortcutsOpen, helpOpen, setCurrentView, trackAction]);
}, [searchOpen, shortcutsOpen, helpOpen, currentView, setCurrentView, trackAction]);
// Debounced search
useEffect(() => {
+26
View File
@@ -580,6 +580,21 @@ export default function ClassesView() {
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">{t('polish.empty_title_no_data')}</h3>
<p className="text-sm text-muted-foreground max-w-md mx-auto leading-relaxed">{t('classes.no_classes')}</p>
{/* Illustration-like decorative elements */}
<div className="mt-6 flex items-center justify-center gap-3">
<div className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-emerald-50/60 dark:bg-emerald-900/20 border border-emerald-200/30 dark:border-emerald-800/20">
<Users className="h-4 w-4 text-emerald-500 dark:text-emerald-400" />
<span className="text-[11px] text-emerald-600/70 dark:text-emerald-400/60">{locale === 'de' ? 'Klasse erstellen' : 'Create class'}</span>
</div>
<div className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-teal-50/60 dark:bg-teal-900/20 border border-teal-200/30 dark:border-teal-800/20">
<UserPlus className="h-4 w-4 text-teal-500 dark:text-teal-400" />
<span className="text-[11px] text-teal-600/70 dark:text-teal-400/60">{locale === 'de' ? 'Schüler hinzufuegen' : 'Add students'}</span>
</div>
<div className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-amber-50/60 dark:bg-amber-900/20 border border-amber-200/30 dark:border-amber-800/20">
<PenLine className="h-4 w-4 text-amber-500 dark:text-amber-400" />
<span className="text-[11px] text-amber-600/70 dark:text-amber-400/60">{locale === 'de' ? 'Fortschritte loggen' : 'Log progress'}</span>
</div>
</div>
<div className="mt-4 flex justify-center">
<div className="h-1 w-16 rounded-full bg-gradient-to-r from-emerald-300 to-teal-300 dark:from-emerald-600 dark:to-teal-600" />
</div>
@@ -614,6 +629,17 @@ export default function ClassesView() {
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{t('label.grade')} {cls.gradeLevel} · {cls.schoolYear?.label}
</p>
{/* Student count + subject count with icons */}
<div className="flex items-center gap-3 mt-2">
<span className="inline-flex items-center gap-1 text-[11px] text-emerald-600/70 dark:text-emerald-400/60">
<Users className="h-3 w-3" />
{cls.studentCount ?? 0} {t('label.student_count').toLowerCase()}
</span>
<span className="inline-flex items-center gap-1 text-[11px] text-teal-600/70 dark:text-teal-400/60">
<BookOpen className="h-3 w-3" />
{cls._count?.competencyAssignments ?? 0} {locale === 'de' ? 'Faecher' : 'subjects'}
</span>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
{/* Small progress indicator */}
+41 -1
View File
@@ -599,6 +599,14 @@ export default function CompetenceFlowerView() {
</CardHeader>
<CardContent>
<div ref={chartRef} className="w-full relative">
{/* Shimmer overlay on load */}
<motion.div
initial={{ opacity: 0.6 }}
animate={{ opacity: 0 }}
transition={{ duration: 1.5, delay: 0.3 }}
className="absolute inset-0 z-10 pointer-events-none rounded-xl bg-gradient-to-r from-transparent via-white/30 to-transparent"
style={{ backgroundSize: '200% 100%', animation: 'shimmer 2s linear' }}
/>
<ResponsiveContainer width="100%" height={400}>
<RadarChart data={chartData} cx="50%" cy="50%" outerRadius="70%">
<defs>
@@ -614,6 +622,10 @@ export default function CompetenceFlowerView() {
<stop offset="0%" stopColor="#f59e0b" stopOpacity={0.25} />
<stop offset="100%" stopColor="#f59e0b" stopOpacity={0.05} />
</linearGradient>
{/* Shadow filter for chart */}
<filter id="chartShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="6" floodColor={primaryColor} floodOpacity="0.12" />
</filter>
</defs>
<PolarGrid stroke="#e5e7eb" strokeDasharray="3 3" />
<PolarAngleAxis
@@ -635,6 +647,7 @@ export default function CompetenceFlowerView() {
fillOpacity={1}
strokeWidth={2.5}
dot={{ r: 4, fill: primaryColor, stroke: '#fff', strokeWidth: 2 }}
filter="url(#chartShadow)"
/>
{/* Class average comparison line */}
{showClassAverage && (
@@ -667,7 +680,8 @@ export default function CompetenceFlowerView() {
borderRadius: '12px',
border: '1px solid #e5e7eb',
fontSize: '12px',
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1)',
boxShadow: '0 4px 12px -2px rgba(0,0,0,0.12), 0 0 0 1px rgba(16,185,129,0.1)',
padding: '10px 14px',
}}
formatter={(value: number, _name: string, props: { payload?: { trueValue?: number; assessed?: number; total?: number } }) => {
const p = props?.payload ?? {};
@@ -740,6 +754,32 @@ export default function CompetenceFlowerView() {
))}
</div>
</div>
{/* Mastery Summary Section */}
<div className="mt-4 grid grid-cols-2 sm:grid-cols-4 gap-3">
{[
{ label: locale === 'de' ? 'Gesamtdurchschnitt' : 'Overall Average', value: overallAverage.toFixed(2), icon: Target, color: primaryColor, bg: `rgba(${parseInt(primaryColor.slice(1,3),16)},${parseInt(primaryColor.slice(3,5),16)},${parseInt(primaryColor.slice(5,7),16)},0.1)` },
{ label: locale === 'de' ? 'Kategorien' : 'Categories', value: flowerData.categories.length, icon: BarChart3, color: '#10b981', bg: 'rgba(16,185,129,0.1)' },
{ label: locale === 'de' ? 'Erfasst' : 'Assessed', value: flowerData.categories.reduce((s, c) => s + c.assessedCompetencyCount, 0), icon: Check, color: '#14b8a6', bg: 'rgba(20,184,166,0.1)' },
{ label: locale === 'de' ? 'Gesamt' : 'Total', value: flowerData.categories.reduce((s, c) => s + c.competencyCount, 0), icon: Star, color: '#f59e0b', bg: 'rgba(245,158,11,0.1)' },
].map((item, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 + i * 0.05, duration: 0.3 }}
className="p-3 rounded-xl border border-gray-200/40 dark:border-gray-700/30 bg-white/60 dark:bg-gray-800/30"
>
<div className="flex items-center gap-2 mb-1.5">
<div className="flex items-center justify-center w-7 h-7 rounded-lg" style={{ backgroundColor: item.bg }}>
<item.icon className="h-3.5 w-3.5" style={{ color: item.color }} />
</div>
<p className="text-[10px] uppercase tracking-wider font-semibold text-gray-500/70 dark:text-gray-400/60 truncate">{item.label}</p>
</div>
<p className="text-lg font-bold text-gray-900 dark:text-gray-100">{item.value}</p>
</motion.div>
))}
</div>
</CardContent>
</Card>
</motion.div>
+129 -26
View File
@@ -37,6 +37,10 @@ import {
Star,
MessageSquare,
Timer,
Globe,
Palette,
Notebook,
CircleDot,
} from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -654,6 +658,51 @@ export default function DashboardView() {
</Card>
</motion.div>
{/* ===== TODAY'S SCHEDULE ===== */}
<motion.div variants={itemVariants}>
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-amber-500 overflow-hidden ring-1 ring-black/[0.03] dark:ring-white/[0.05] card-hover-lift">
<CardHeader className="pb-3 pt-5 bg-gradient-to-r from-amber-50/50 via-emerald-50/20 to-transparent dark:from-amber-900/10 dark:via-emerald-900/5 dark:to-transparent">
<CardTitle className="text-lg font-bold flex items-center gap-2">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-amber-400 to-emerald-500 text-white shadow-sm">
<CalendarDays className="h-4 w-4" />
</div>
{locale === 'de' ? 'Heutige Aufgaben' : 'Today\'s Schedule'}
<span className="text-xs font-normal text-gray-500 dark:text-gray-400 ml-1 hidden sm:inline">
· {new Date().toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-GB', { weekday: 'short', day: 'numeric', month: 'short' })}
</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{[
{ icon: PenLine, label: locale === 'de' ? 'Fortschritte erfassen' : 'Log progress entries', desc: locale === 'de' ? 'Neue Beobachtungen eintragen' : 'Record new observations', view: 'progress', color: 'from-emerald-400 to-teal-500' },
{ icon: ClipboardCheck, label: locale === 'de' ? 'Uberpruefung erstellen' : 'Create assessment', desc: locale === 'de' ? 'Leistungsueberpruefung planen' : 'Plan performance assessment', view: 'assessments', color: 'from-amber-400 to-rose-500' },
{ icon: BookOpen, label: locale === 'de' ? 'Kompetenzblume ansehen' : 'View competence flower', desc: locale === 'de' ? 'Radar-Chart pruefen' : 'Check radar chart', view: 'flower', color: 'from-violet-400 to-emerald-500' },
{ icon: Notebook, label: locale === 'de' ? 'Notizbuch aktualisieren' : 'Update notebook', desc: locale === 'de' ? 'Hefte bearbeiten' : 'Edit notebooks', view: 'notebooks', color: 'from-teal-400 to-emerald-500' },
].map((item, i) => (
<motion.button
key={i}
whileHover={{ scale: 1.01, x: 2 }}
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.15 }}
onClick={() => setCurrentView(item.view)}
className="w-full group flex items-center gap-3 p-3 rounded-xl bg-gradient-to-r from-gray-50/80 to-gray-50/0 dark:from-gray-800/50 dark:to-gray-800/0 border border-gray-100/60 dark:border-gray-800/40 hover:border-emerald-200/60 dark:hover:border-emerald-800/30 transition-all relative overflow-hidden"
>
<div className={`flex items-center justify-center w-9 h-9 rounded-lg bg-gradient-to-br ${item.color} text-white shadow-sm shrink-0 transition-transform duration-200 group-hover:scale-110`}>
<item.icon className="h-4 w-4" />
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate">{item.label}</p>
<p className="text-[11px] text-gray-500 dark:text-gray-400 truncate">{item.desc}</p>
</div>
<ChevronRight className="h-4 w-4 text-gray-300 dark:text-gray-600 group-hover:text-emerald-500 transition-colors shrink-0" />
</motion.button>
))}
</div>
</CardContent>
</Card>
</motion.div>
{/* ===== QUICK ACTION BUTTONS ===== */}
<motion.div variants={itemVariants} className="flex flex-col sm:flex-row flex-wrap gap-3">
<motion.div whileHover={{ scale: 1.02, y: -1 }} whileTap={{ scale: 0.98 }} transition={{ duration: 0.15 }}>
@@ -1145,34 +1194,88 @@ export default function DashboardView() {
</Card>
</motion.div>
{/* ===== ENVIRONMENTAL TIPS SECTION ===== */}
<motion.div variants={itemVariants}>
<Card className="border-0 shadow-sm rounded-xl overflow-hidden bg-white dark:bg-gray-950 ring-1 ring-black/[0.03] dark:ring-white/[0.05]">
<CardContent className="p-5">
<div className="flex items-center gap-4">
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-400 to-teal-500 text-white shadow-md shrink-0">
<Leaf className="h-6 w-6" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-bold text-emerald-700 dark:text-emerald-300 mb-1">
{t('dashboard.env_tip_title')}
</h3>
<p className="text-xs text-gray-600 dark:text-gray-400 leading-relaxed">
{t('dashboard.env_tip_text')}
</p>
</div>
<div className="hidden md:flex items-center gap-2 shrink-0">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400">
<TreePine className="h-4 w-4" />
</div>
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-teal-100 dark:bg-teal-900/30 text-teal-600 dark:text-teal-400">
<Sprout className="h-4 w-4" />
{/* ===== PAPER SAVED & ENVIRONMENTAL SECTION ===== */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Paper Saved Counter */}
<motion.div variants={itemVariants}>
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-emerald-500 overflow-hidden bg-white dark:bg-gray-950 ring-1 ring-black/[0.03] dark:ring-white/[0.05] card-hover-lift">
<CardContent className="p-6">
<div className="flex items-center gap-4">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.4, type: 'spring', stiffness: 150 }}
className="flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-emerald-400 to-teal-500 text-white shadow-lg shadow-emerald-300/30 shrink-0"
>
<Leaf className="h-8 w-8" />
</motion.div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-bold text-emerald-700 dark:text-emerald-300 mb-1">
{locale === 'de' ? 'Papier gespart' : 'Paper Saved'}
</h3>
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold text-gray-900 dark:text-gray-100">
{data.stats.totalProgressEntries * 50}
</span>
<span className="text-sm text-emerald-600/70 dark:text-emerald-400/60 font-medium">
{locale === 'de' ? 'Seiten' : 'pages'}
</span>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 leading-relaxed mt-2">
{locale === 'de'
? 'Jeder digitale Eintrag spart ca. 50 Seiten Papier. Danke fuer Ihren Beitrag zum Umweltschutz!'
: 'Each digital entry saves ~50 pages of paper. Thank you for contributing to environmental protection!'}
</p>
<div className="flex items-center gap-2 mt-3">
<div className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-emerald-100/60 dark:bg-emerald-900/30 text-[11px] font-semibold text-emerald-600 dark:text-emerald-400">
<TreePine className="h-3 w-3" />
{locale === 'de' ? '~0.5 Baume' : '~0.5 trees'}
</div>
<div className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-teal-100/60 dark:bg-teal-900/30 text-[11px] font-semibold text-teal-600 dark:text-teal-400">
<Sprout className="h-3 w-3" />
{locale === 'de' ? 'CO2 reduziert' : 'CO2 reduced'}
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
</CardContent>
</Card>
</motion.div>
{/* Environmental Tips — visually enhanced */}
<motion.div variants={itemVariants}>
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-teal-500 overflow-hidden bg-white dark:bg-gray-950 ring-1 ring-black/[0.03] dark:ring-white/[0.05] card-hover-lift">
<CardContent className="p-6">
<div className="flex items-start gap-4">
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-gradient-to-br from-teal-400 to-emerald-500 text-white shadow-md shrink-0">
<TreePine className="h-6 w-6" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-bold text-emerald-700 dark:text-emerald-300 mb-1">
{t('dashboard.env_tip_title')}
</h3>
<p className="text-xs text-gray-600 dark:text-gray-400 leading-relaxed">
{t('dashboard.env_tip_text')}
</p>
<div className="grid grid-cols-2 gap-2 mt-3">
{[
{ icon: Leaf, text: locale === 'de' ? 'Digital statt Papier' : 'Digital instead of paper' },
{ icon: Sprout, text: locale === 'de' ? 'Nachhaltig dokumentieren' : 'Sustainable documentation' },
{ icon: TreePine, text: locale === 'de' ? 'Baeume schuetzen' : 'Protect trees' },
{ icon: Trees, text: locale === 'de' ? 'CO2 einsparen' : 'Reduce CO2' },
].map((tip, i) => (
<div key={i} className="flex items-center gap-1.5 px-2 py-1.5 rounded-lg bg-emerald-50/60 dark:bg-emerald-900/20 border border-emerald-200/30 dark:border-emerald-800/20">
<tip.icon className="h-3.5 w-3.5 text-emerald-500 dark:text-emerald-400 shrink-0" />
<span className="text-[11px] text-emerald-700/80 dark:text-emerald-300/80 font-medium">{tip.text}</span>
</div>
))}
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</motion.div>
);
}
@@ -30,6 +30,10 @@ import {
MessageSquareText,
CalendarDays,
Settings,
Palette,
FileDown,
Archive,
ArrowLeft,
} from 'lucide-react';
import {
Dialog,
@@ -120,6 +124,18 @@ export default function KeyboardShortcutsDialog({ open, onOpenChange }: Keyboard
{ keys: ['Esc'], labelKey: 'shortcuts.close_dialog', icon: X },
],
},
{
titleKey: 'shortcuts.notebooks_drawing',
icon: BookOpen,
entries: [
{ keys: ['Cmd/Ctrl', 'N'], labelKey: 'shortcuts.new_notebook', icon: BookOpen },
{ keys: ['Cmd/Ctrl', 'D'], labelKey: 'shortcuts.drawing', icon: Palette },
{ keys: ['Cmd/Ctrl', 'Shift', 'N'], labelKey: 'shortcuts.new_page', icon: Plus },
{ keys: ['Cmd/Ctrl', 'E'], labelKey: 'shortcuts.export_pdf', icon: FileDown },
{ keys: ['Cmd/Ctrl', 'Shift', 'A'], labelKey: 'shortcuts.archive_toggle', icon: Archive },
{ keys: ['Cmd/Ctrl', '.'], labelKey: 'shortcuts.close_notebook', icon: ArrowLeft },
],
},
];
return (
File diff suppressed because it is too large. Load diff
+57 -4
View File
@@ -2,7 +2,12 @@
import React, { useState, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, BookOpen, BarChart3, Pencil, Flower2, FileText, PartyPopper } from 'lucide-react';
import {
X, BookOpen, BarChart3, Pencil, Flower2, FileText, PartyPopper,
Users, ClipboardCheck, GraduationCap, Palette, Leaf,
TreePine, CalendarDays, MessageSquare, BarChart, Target,
Zap, Award, Shield, Notebook,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Dialog,
@@ -35,6 +40,12 @@ const steps: OnboardingStep[] = [
descKey: 'onboarding.dashboard_desc',
accent: 'from-teal-400 to-emerald-500',
},
{
iconComponent: Users,
titleKey: 'onboarding.classes_title',
descKey: 'onboarding.classes_desc',
accent: 'from-emerald-400 to-cyan-500',
},
{
iconComponent: Pencil,
titleKey: 'onboarding.progress_title',
@@ -47,12 +58,54 @@ const steps: OnboardingStep[] = [
descKey: 'onboarding.flower_desc',
accent: 'from-rose-400 to-emerald-500',
},
{
iconComponent: ClipboardCheck,
titleKey: 'onboarding.assessments_title',
descKey: 'onboarding.assessments_desc',
accent: 'from-amber-400 to-rose-500',
},
{
iconComponent: GraduationCap,
titleKey: 'onboarding.grading_title',
descKey: 'onboarding.grading_desc',
accent: 'from-violet-400 to-emerald-500',
},
{
iconComponent: FileText,
titleKey: 'onboarding.reports_title',
descKey: 'onboarding.reports_desc',
accent: 'from-violet-400 to-emerald-500',
},
{
iconComponent: Notebook,
titleKey: 'onboarding.notebooks_title',
descKey: 'onboarding.notebooks_desc',
accent: 'from-teal-400 to-emerald-500',
},
{
iconComponent: Palette,
titleKey: 'onboarding.drawing_title',
descKey: 'onboarding.drawing_desc',
accent: 'from-rose-400 to-violet-500',
},
{
iconComponent: CalendarDays,
titleKey: 'onboarding.calendar_title',
descKey: 'onboarding.calendar_desc',
accent: 'from-amber-400 to-teal-500',
},
{
iconComponent: MessageSquare,
titleKey: 'onboarding.parents_title',
descKey: 'onboarding.parents_desc',
accent: 'from-cyan-400 to-emerald-500',
},
{
iconComponent: Leaf,
titleKey: 'onboarding.environmental_title',
descKey: 'onboarding.environmental_desc',
accent: 'from-emerald-400 to-green-500',
},
{
iconComponent: PartyPopper,
titleKey: 'onboarding.done_title',
@@ -130,12 +183,12 @@ export default function OnboardingTour({ open, onClose }: OnboardingTourProps) {
transition={{ duration: 0.3, ease: 'easeInOut' }}
className="flex flex-col items-center text-center px-8 pt-10 pb-6"
>
{/* Emoji illustration */}
{/* Icon illustration */}
<motion.div
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.15, duration: 0.4, type: 'spring', stiffness: 200 }}
className={`w-24 h-24 rounded-3xl bg-gradient-to-br ${step.accent} flex items-center justify-center text-5xl shadow-lg mb-6`}
className={`w-24 h-24 rounded-3xl bg-gradient-to-br ${step.accent} flex items-center justify-center shadow-lg mb-6`}
>
<step.iconComponent className="w-10 h-10 text-white" />
</motion.div>
@@ -164,7 +217,7 @@ export default function OnboardingTour({ open, onClose }: OnboardingTourProps) {
</div>
{/* Step indicator dots */}
<div className="flex items-center justify-center gap-2 pb-4">
<div className="flex items-center justify-center gap-1.5 pb-4 flex-wrap px-4">
{steps.map((_, i) => (
<button
key={i}
+56 -2
View File
@@ -6,7 +6,7 @@ import {
PenLine, User, Clock, Plus, ChevronDown, BadgeCheck,
Download, CalendarDays, Keyboard, Sparkles, BookOpen,
GraduationCap, Trash2, CheckSquare, X, ListChecks,
Sprout, Leaf, TreePine, Trees,
Sprout, Leaf, TreePine, Trees, BarChart3,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -675,7 +675,61 @@ export default function ProgressEntriesView() {
</Card>
{/* Student timeline */}
<div className="lg:col-span-2">
<div className="lg:col-span-2 space-y-6">
{/* Mastery Distribution mini chart */}
{selectedStudent && studentEntries.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-emerald-500 overflow-hidden card-hover-lift">
<CardContent className="p-4">
<p className="text-[10px] uppercase tracking-wider font-semibold text-emerald-600/70 dark:text-emerald-400/60 mb-3 flex items-center gap-1.5">
<BarChart3 className="h-3 w-3" />
{locale === 'de' ? 'Mastery-Verteilung' : 'Mastery Distribution'}
</p>
<div className="flex items-center gap-3">
{[1, 2, 3, 4].map((level) => {
const count = studentEntries.filter((e) => e.masteryLevelValue === level).length;
const pct = studentEntries.length > 0 ? (count / studentEntries.length) * 100 : 0;
const colors = [
'bg-red-400 dark:bg-red-500',
'bg-amber-400 dark:bg-amber-500',
'bg-emerald-400 dark:bg-emerald-500',
'bg-teal-400 dark:bg-teal-500',
];
const labelColors = [
'text-red-600 dark:text-red-400',
'text-amber-600 dark:text-amber-400',
'text-emerald-600 dark:text-emerald-400',
'text-teal-600 dark:text-teal-400',
];
return (
<div key={level} className="flex-1 text-center">
<div className="h-16 bg-gray-100 dark:bg-gray-800/50 rounded-lg overflow-hidden relative mb-1">
<motion.div
initial={{ height: 0 }}
animate={{ height: `${Math.max(pct, 2)}%` }}
transition={{ duration: 0.5, delay: 0.1 + level * 0.08 }}
className={`absolute bottom-0 left-0 right-0 ${colors[level - 1]} rounded-t-md`}
/>
<span className="absolute inset-0 flex items-center justify-center text-xs font-bold text-gray-700 dark:text-gray-300">
{count}
</span>
</div>
<span className={`text-[10px] font-semibold ${labelColors[level - 1]}`}>
{getMasteryIcon(level)} {level}
</span>
</div>
);
})}
</div>
</CardContent>
</Card>
</motion.div>
)}
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-teal-500 overflow-hidden">
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-teal-50/50 to-transparent dark:from-teal-900/10 dark:to-transparent">
<div className="flex flex-wrap items-center justify-between gap-2">