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:
1 parent
04034dca62
commit
62f4a150e0
14 files changed
+1983
-166
No files matched your search
@@ -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(() => {
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
+1022
-121
File diff suppressed because it is too large.
Load diff
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user