Round 19: Competition system, German curriculum learning, enhanced notebooks, student dashboard
- Fix portfolio-view.tsx: replace entry.isPublic with form.isPublic (runtime error) - Add Competition system: 5 Prisma models (Competition, CompetitionParticipant, CompetitionReward, CompetitionLeaderboard, RewardClaim) - Add German curriculum learning: 4 Prisma models (SubjectTopic, SubjectLesson, LessonQuestion, StudentAnswer) - Add 12 API routes for competitions and rewards - Add 7 API routes for subject topics, lessons, questions, and student answers - Create competitions-view.tsx with full CRUD, leaderboard, rewards, registration - Enhance notebooks-view.tsx with 'Lernen' tab for German curriculum subjects - Add student-specific dashboard with Papier sparen banner, notebook quick access, learning progress - Add competitions to teacher and student navigation - Add 130+ i18n keys for DE and EN (competitions, rewards, learning, environment) - Enhanced student nav with competitions and notebooks - DSGVO-compliant reward claims with status lifecycle
This commit is contained in:
1 parent
74404708e5
commit
849b48a5a4
22 files changed
+6543
-5
No files matched your search
@@ -57,6 +57,7 @@ import {
|
||||
Briefcase,
|
||||
Clock as ClockIcon,
|
||||
Library as LibraryIcon,
|
||||
Trophy,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -150,6 +151,7 @@ import HomeworkView from './homework-view';
|
||||
import PortfolioView from './portfolio-view';
|
||||
import TimetableView from './timetable-view';
|
||||
import ResourceLibraryView from './resource-library-view';
|
||||
import CompetitionsView from './competitions-view';
|
||||
|
||||
// Dynamic imports for heavy components with loading skeletons
|
||||
const AnalyticsView = dynamic(() => import('./analytics-view'), {
|
||||
@@ -207,6 +209,7 @@ const navSections: NavSection[] = [
|
||||
{ key: 'portfolio', icon: Briefcase, labelKey: 'nav.portfolio' },
|
||||
{ key: 'timetable', icon: ClockIcon, labelKey: 'nav.timetable' },
|
||||
{ key: 'resources', icon: LibraryIcon, labelKey: 'nav.resources' },
|
||||
{ key: 'competitions', icon: Trophy, labelKey: 'nav.competitions' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -233,6 +236,7 @@ const studentNavSections: NavSection[] = [
|
||||
{ key: 'attendance', icon: CalendarCheck, labelKey: 'nav.student_attendance' },
|
||||
{ key: 'portfolio', icon: Briefcase, labelKey: 'nav.portfolio' },
|
||||
{ key: 'calendar', icon: CalendarIconNav, labelKey: 'nav.calendar' },
|
||||
{ key: 'competitions', icon: Trophy, labelKey: 'nav.competitions' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -376,6 +380,7 @@ function renderView(view: ViewName) {
|
||||
case 'portfolio': return <PortfolioView />;
|
||||
case 'timetable': return <TimetableView />;
|
||||
case 'resources': return <ResourceLibraryView />;
|
||||
case 'competitions': return <CompetitionsView />;
|
||||
default: return <DashboardView />;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -664,6 +664,47 @@ export default function DashboardView() {
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* ── Environmental Banner (Papier sparen) ─────────────────────── */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-r from-emerald-500 via-emerald-600 to-teal-600 dark:from-emerald-700 dark:via-emerald-800 dark:to-teal-700 shadow-lg shadow-emerald-300/30 dark:shadow-emerald-900/40">
|
||||
{/* Decorative leaf pattern */}
|
||||
<div className="absolute inset-0 opacity-10 pointer-events-none">
|
||||
<div className="absolute top-2 right-8">
|
||||
<Leaf className="h-24 w-24 text-white rotate-12" />
|
||||
</div>
|
||||
<div className="absolute bottom-1 right-40">
|
||||
<Leaf className="h-16 w-16 text-white -rotate-45" />
|
||||
</div>
|
||||
<div className="absolute top-4 right-72">
|
||||
<Leaf className="h-10 w-10 text-white rotate-90" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative p-5 sm:p-6 flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-white/20 backdrop-blur-sm shrink-0">
|
||||
<Leaf className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-lg font-bold text-white leading-tight">
|
||||
{t('student.papier_sparen_title')}
|
||||
</h3>
|
||||
<p className="text-sm text-emerald-100/90 mt-0.5 leading-snug">
|
||||
{t('student.papier_sparen_subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setCurrentView('notebooks')}
|
||||
className="shrink-0 bg-white/20 hover:bg-white/30 text-white border-white/30 backdrop-blur-sm min-h-[44px] px-5 font-semibold transition-all duration-200 hover:shadow-lg"
|
||||
variant="outline"
|
||||
>
|
||||
<BookOpen className="h-4 w-4 mr-2" />
|
||||
{t('student.go_to_notebooks')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Student Quick Stats */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="relative p-4 rounded-xl bg-mesh border border-emerald-200/40 dark:border-emerald-900/30 overflow-hidden backdrop-blur-sm">
|
||||
@@ -689,6 +730,130 @@ export default function DashboardView() {
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* ── Student Notebook Quick Access (Meine Hefte) ─────────────── */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card className="border-0 shadow-sm rounded-xl overflow-hidden ring-1 ring-black/[0.03] dark:ring-white/[0.05]">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center w-9 h-9 rounded-lg bg-gradient-to-br from-teal-400 to-emerald-500 text-white shadow-sm">
|
||||
<BookMarked className="h-4 w-4" />
|
||||
</div>
|
||||
<CardTitle className="text-sm font-semibold">{t('student.my_notebooks_quick')}</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setCurrentView('notebooks')}
|
||||
className="text-xs text-emerald-600 dark:text-emerald-400 hover:text-emerald-700 dark:hover:text-emerald-300 min-h-[44px] px-2"
|
||||
>
|
||||
{t('student.show_all_notebooks')}
|
||||
<ChevronRight className="h-3.5 w-3.5 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-1">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ title: t('student.notebook_math'), subject: 'Mathematik', color: 'from-amber-400 to-orange-500', icon: Calculator },
|
||||
{ title: t('student.notebook_german'), subject: 'Deutsch', color: 'from-emerald-400 to-teal-500', icon: BookOpen },
|
||||
{ title: t('student.notebook_english'), subject: 'Englisch', color: 'from-rose-400 to-pink-500', icon: Globe },
|
||||
{ title: t('student.notebook_science'), subject: 'Naturwissenschaften', color: 'from-violet-400 to-purple-500', icon: Flower2 },
|
||||
].map((notebook) => (
|
||||
<motion.div
|
||||
key={notebook.subject}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="cursor-pointer min-h-[44px]"
|
||||
onClick={() => setCurrentView('notebooks')}
|
||||
>
|
||||
<div className="relative rounded-xl overflow-hidden border border-gray-200/40 dark:border-gray-700/40 bg-white dark:bg-gray-900/50 hover:shadow-md transition-shadow duration-200">
|
||||
{/* Notebook cover color strip */}
|
||||
<div className={`h-2 bg-gradient-to-r ${notebook.color}`} />
|
||||
<div className="p-3">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<div className={`flex items-center justify-center w-7 h-7 rounded-md bg-gradient-to-br ${notebook.color} text-white shadow-sm`}>
|
||||
<notebook.icon className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-gray-800 dark:text-gray-200 truncate">{notebook.title}</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-500 dark:text-gray-400 truncate">{notebook.subject}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 flex justify-center">
|
||||
<Button
|
||||
onClick={() => setCurrentView('notebooks')}
|
||||
className="bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white min-h-[44px] px-6 font-semibold shadow-sm transition-all duration-200"
|
||||
>
|
||||
<BookOpen className="h-4 w-4 mr-2" />
|
||||
{t('student.start_learning')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* ── Student Learning Progress ────────────────────────────────── */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card className="border-0 shadow-sm rounded-xl overflow-hidden ring-1 ring-black/[0.03] dark:ring-white/[0.05]">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center w-9 h-9 rounded-lg bg-gradient-to-br from-emerald-400 to-teal-500 text-white shadow-sm">
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<CardTitle className="text-sm font-semibold">{t('student.learning_progress')}</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setCurrentView('flower')}
|
||||
className="text-xs text-emerald-600 dark:text-emerald-400 hover:text-emerald-700 dark:hover:text-emerald-300 min-h-[44px] px-2"
|
||||
>
|
||||
{t('student.continue_learning')}
|
||||
<ChevronRight className="h-3.5 w-3.5 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-1">
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ subject: t('student.notebook_math'), progress: 72, color: 'bg-amber-500', bgColor: 'bg-amber-100 dark:bg-amber-900/30' },
|
||||
{ subject: t('student.notebook_german'), progress: 85, color: 'bg-emerald-500', bgColor: 'bg-emerald-100 dark:bg-emerald-900/30' },
|
||||
{ subject: t('student.notebook_english'), progress: 58, color: 'bg-rose-500', bgColor: 'bg-rose-100 dark:bg-rose-900/30' },
|
||||
{ subject: t('student.notebook_science'), progress: 44, color: 'bg-violet-500', bgColor: 'bg-violet-100 dark:bg-violet-900/30' },
|
||||
].map((item) => (
|
||||
<div key={item.subject} className="flex items-center gap-3 min-h-[44px]">
|
||||
<span className="text-xs font-medium text-gray-700 dark:text-gray-300 w-28 shrink-0 truncate">{item.subject}</span>
|
||||
<div className="flex-1 h-3 rounded-full bg-gray-100 dark:bg-gray-800 overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${item.progress}%` }}
|
||||
transition={{ duration: 0.8, ease: 'easeOut' }}
|
||||
className={`h-full rounded-full ${item.color}`}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-gray-600 dark:text-gray-400 w-9 text-right shrink-0">{item.progress}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t border-gray-100 dark:border-gray-800 flex items-center justify-center">
|
||||
<Button
|
||||
onClick={() => setCurrentView('flower')}
|
||||
variant="outline"
|
||||
className="text-emerald-600 dark:text-emerald-400 border-emerald-200 dark:border-emerald-800 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 min-h-[44px] px-5 font-semibold transition-all duration-200"
|
||||
>
|
||||
<GraduationCap className="h-4 w-4 mr-2" />
|
||||
{t('student.continue_learning')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Student Cards Grid */}
|
||||
<motion.div variants={itemVariants} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{/* My Competencies Card */}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -578,7 +578,7 @@ export default function PortfolioView() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-medium">{entry.isPublic ? t('portfolio.public') : t('portfolio.private')}</Label>
|
||||
<Label className="text-xs font-medium">{form.isPublic ? t('portfolio.public') : t('portfolio.private')}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<EyeOff className="h-4 w-4 text-gray-400" />
|
||||
<Switch
|
||||
|
||||
Reference in new issue
Block a user