Round 24: Data Import/Export, School Events, Student Wellness, Styling

- Data Import/Export View: CSV/JSON import with drag-and-drop, column mapping, data preview, validation, export with filters, database statistics
- School Event Management View: Calendar/list views, event CRUD with recurring support, registration, feedback, budget tracking
- Student Wellness View: Daily check-in (mood, sleep, stress, activity), wellness score gauge, health records, trend charts
- 4 new Prisma models: DataImportJob, DataExportJob, WellnessCheckin, WellnessScore
- Enhanced SchoolEvent model with budget, registration, recurring, and status fields
- Added EventFeedback model
- Enhanced auth-view styling with glassmorphism card and animated floating shapes
- Enhanced dashboard-view with gradient header, animated stat counters, welcome section
- Added global CSS utility classes and keyframe animations
- 14+ new API route files
- 300+ i18n keys added
- All views verified working in browser
This commit is contained in:
Mustafa Cetin committed 2026-07-30 03:05:16 +00:00
1 parent 7ea8dc48c8
commit a159ed7fe6
22 files changed
+8349 -153

No files matched your search

+14
View File
@@ -68,6 +68,8 @@ import {
Timer,
LayoutGrid as LayoutGridIcon,
BookOpen as BookOpenIconNav,
Database,
PartyPopper,
} from 'lucide-react';
import {
Sidebar,
@@ -182,6 +184,9 @@ import StudentStudyPlannerView from './student-study-planner-view';
import ReportCardView from './report-card-view';
import SchoolLibraryView from './school-library-view';
import SeatingChartView from './seating-chart-view';
import StudentWellnessView from './student-wellness-view';
import DataImportExportView from './data-import-export-view';
import SchoolEventsView from './school-events-view';
// Dynamic imports for heavy components with loading skeletons
const AnalyticsView = dynamic(() => import('./analytics-view'), {
@@ -251,8 +256,11 @@ const navSections: NavSection[] = [
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
{ key: 'seating-chart', icon: LayoutGridIcon, labelKey: 'nav.seating_chart' },
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
{ key: 'student-wellness', icon: Heart, labelKey: 'nav.student-wellness' },
{ key: 'notification-center', icon: Bell, labelKey: 'nav.notification_center' },
{ key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' },
{ key: 'school-events', icon: PartyPopper, labelKey: 'nav.school-events' },
{ key: 'data-import-export', icon: Database, labelKey: 'nav.data-import-export' },
],
},
{
@@ -293,8 +301,10 @@ const studentNavSections: NavSection[] = [
{ key: 'seating-chart', icon: LayoutGridIcon, labelKey: 'nav.seating_chart' },
{ key: 'student-achievements', icon: Trophy, labelKey: 'achievements.title' },
{ key: 'student-study-planner', icon: Timer, labelKey: 'study_planner.title' },
{ key: 'student-wellness', icon: Heart, labelKey: 'nav.student-wellness' },
{ key: 'resources', icon: FolderOpen, labelKey: 'nav.resources' },
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
{ key: 'school-events', icon: PartyPopper, labelKey: 'nav.school-events' },
{ key: 'notification-center', icon: Bell, labelKey: 'nav.notification_center' },
{ key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' },
{ key: 'settings', icon: Settings, labelKey: 'nav.settings' },
@@ -321,6 +331,7 @@ const parentNavSections: NavSection[] = [
{ key: 'student-study-planner', icon: Timer, labelKey: 'study_planner.title' },
{ key: 'report-cards', icon: FileText, labelKey: 'nav.report_cards' },
{ key: 'school-library', icon: LibraryIcon, labelKey: 'nav.school_library' },
{ key: 'school-events', icon: PartyPopper, labelKey: 'nav.school-events' },
{ key: 'notification-center', icon: Bell, labelKey: 'nav.notification_center' },
{ key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' },
],
@@ -471,6 +482,9 @@ function renderView(view: ViewName) {
case 'seating-chart': return <SeatingChartView />;
case 'school-library': return <SchoolLibraryView />;
case 'report-cards': return <ReportCardView />;
case 'student-wellness': return <StudentWellnessView />;
case 'data-import-export': return <DataImportExportView />;
case 'school-events': return <SchoolEventsView />;
default: return <DashboardView />;
}
}
+232 -9
View File
@@ -2,11 +2,12 @@
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { BookOpen, Mail, Lock, User, ArrowRight, GraduationCap, Heart, Sparkles, Flower2, Eye, EyeOff, KeyRound, Info, Shield, Leaf, Users as UsersIcon, HelpCircle, BarChart3, LockKeyhole, GitBranch } from 'lucide-react';
import { BookOpen, Mail, Lock, User, ArrowRight, GraduationCap, Heart, Sparkles, Flower2, Eye, EyeOff, KeyRound, Info, Shield, Leaf, Users as UsersIcon, HelpCircle, BarChart3, LockKeyhole, GitBranch, CheckCircle2, XCircle, Globe, Github, Chrome } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import { useAppStore } from '@/lib/store';
@@ -338,8 +339,19 @@ export default function AuthView() {
const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(true);
const [showForgotInfo, setShowForgotInfo] = useState(false);
const [showForgotModal, setShowForgotModal] = useState(false);
const [forgotEmail, setForgotEmail] = useState('');
const [forgotSent, setForgotSent] = useState(false);
const setCurrentUser = useAppStore((s) => s.setCurrentUser);
// Input validation state
const emailValid = email.length > 0 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const emailTouched = email.length > 0;
const passwordValid = password.length >= 6;
const passwordTouched = password.length > 0;
const firstNameValid = firstName.length > 0;
const lastNameValid = lastName.length > 0;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
@@ -591,12 +603,32 @@ export default function AuthView() {
<Label htmlFor="firstName" className="text-sm font-medium">{t('auth.firstName')}</Label>
<div className="relative">
<User className="absolute left-3.5 top-1/2 -translate-y-1/2 h-5 w-5 text-emerald-400" />
<Input id="firstName" value={firstName} onChange={(e) => setFirstName(e.target.value)} className="pl-10 h-12 min-h-[44px] border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20 text-base bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm" placeholder={t('auth.firstName')} required />
<Input id="firstName" value={firstName} onChange={(e) => setFirstName(e.target.value)} className={`pl-10 pr-9 h-12 min-h-[44px] bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm transition-all duration-200 ${
firstName.length > 0 && firstNameValid ? 'input-valid border-emerald-400 dark:border-emerald-500'
: firstName.length > 0 && !firstNameValid ? 'input-invalid border-red-300 dark:border-red-700'
: 'border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20'
}`} placeholder={t('auth.firstName')} required />
{firstName.length > 0 && (
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: 'spring', stiffness: 300, damping: 20 }} className="absolute right-2.5 top-1/2 -translate-y-1/2">
{firstNameValid ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> : <XCircle className="h-4 w-4 text-red-400" />}
</motion.div>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="lastName" className="text-sm font-medium">{t('auth.lastName')}</Label>
<Input id="lastName" value={lastName} onChange={(e) => setLastName(e.target.value)} className="h-12 min-h-[44px] border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20 text-base bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm" placeholder={t('auth.lastName')} required />
<div className="relative">
<Input id="lastName" value={lastName} onChange={(e) => setLastName(e.target.value)} className={`pr-9 h-12 min-h-[44px] bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm transition-all duration-200 ${
lastName.length > 0 && lastNameValid ? 'input-valid border-emerald-400 dark:border-emerald-500'
: lastName.length > 0 && !lastNameValid ? 'input-invalid border-red-300 dark:border-red-700'
: 'border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20'
}`} placeholder={t('auth.lastName')} required />
{lastName.length > 0 && (
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: 'spring', stiffness: 300, damping: 20 }} className="absolute right-2.5 top-1/2 -translate-y-1/2">
{lastNameValid ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> : <XCircle className="h-4 w-4 text-red-400" />}
</motion.div>
)}
</div>
</div>
</motion.div>
</motion.div>
@@ -624,7 +656,7 @@ export default function AuthView() {
)}
</AnimatePresence>
{/* Email field (always shown) */}
{/* Email field (always shown) with validation feedback */}
<motion.div variants={itemVariants} className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium">{t('auth.email')}</Label>
<div className="relative">
@@ -634,7 +666,11 @@ export default function AuthView() {
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className={`pl-10 h-12 min-h-[44px] border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20 text-base bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm transition-all duration-200 ${
className={`pl-10 pr-10 h-12 min-h-[44px] text-base bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm transition-all duration-200 ${
emailTouched && emailValid ? 'input-valid border-emerald-400 dark:border-emerald-500 focus:border-emerald-500 focus:ring-emerald-400/20'
: emailTouched && !emailValid ? 'input-invalid border-red-300 dark:border-red-700 focus:border-red-400 focus:ring-red-400/20'
: 'border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20'
} ${
loginRole === 'student' ? 'focus:border-amber-400 focus:ring-amber-400/20'
: loginRole === 'parent' ? 'focus:border-violet-400 focus:ring-violet-400/20'
: ''
@@ -642,17 +678,32 @@ export default function AuthView() {
placeholder={loginRole === 'student' ? 'name@schule.de' : loginRole === 'parent' ? 'parent@email.de' : 'name@schule.de'}
required
/>
{/* Validation icon */}
{emailTouched && (
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
className="absolute right-3.5 top-1/2 -translate-y-1/2"
>
{emailValid ? (
<CheckCircle2 className="h-5 w-5 text-emerald-500" />
) : (
<XCircle className="h-5 w-5 text-red-400" />
)}
</motion.div>
)}
</div>
</motion.div>
{/* Password field */}
{/* Password field with validation feedback */}
<motion.div variants={itemVariants} className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="password" className="text-sm font-medium">{t('auth.password')}</Label>
{mode === 'login' && (
<motion.button
type="button"
onClick={() => setShowForgotInfo(!showForgotInfo)}
onClick={() => setShowForgotModal(true)}
className="text-xs text-emerald-600 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300 font-medium transition-colors"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
@@ -668,15 +719,34 @@ export default function AuthView() {
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-9 pr-10 h-12 min-h-[44px] border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20 bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm transition-all duration-200"
className={`pl-9 pr-12 h-12 min-h-[44px] bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm transition-all duration-200 ${
passwordTouched && passwordValid ? 'input-valid border-emerald-400 dark:border-emerald-500 focus:border-emerald-500 focus:ring-emerald-400/20'
: passwordTouched && !passwordValid ? 'input-invalid border-red-300 dark:border-red-700 focus:border-red-400 focus:ring-red-400/20'
: 'border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20'
}`}
placeholder="••••••"
minLength={6}
required
/>
{/* Validation icon */}
{passwordTouched && (
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
className="absolute right-10 top-1/2 -translate-y-1/2"
>
{passwordValid ? (
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
) : (
<XCircle className="h-4 w-4 text-red-400" />
)}
</motion.div>
)}
<motion.button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-emerald-400 hover:text-emerald-600 dark:hover:text-emerald-300 transition-colors w-10 h-10 flex items-center justify-center"
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-emerald-400 hover:text-emerald-600 dark:hover:text-emerald-300 transition-colors w-6 h-6 flex items-center justify-center"
tabIndex={-1}
aria-label={showPassword ? t('polish.hide_password') : t('polish.show_password')}
whileHover={{ scale: 1.1 }}
@@ -686,6 +756,30 @@ export default function AuthView() {
</motion.button>
</div>
{/* Password strength indicator (register mode) */}
{mode === 'register' && passwordTouched && (
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} transition={{ duration: 0.2 }}>
<div className="flex gap-1 mt-1">
{[1, 2, 3, 4].map((level) => (
<div
key={level}
className={`h-1 flex-1 rounded-full transition-colors duration-300 ${
password.length >= level * 2
? level <= 1 ? 'bg-red-400'
: level <= 2 ? 'bg-amber-400'
: level <= 3 ? 'bg-emerald-400'
: 'bg-teal-400'
: 'bg-gray-200 dark:bg-gray-700'
}`}
/>
))}
</div>
<p className="text-[10px] mt-1 text-gray-500 dark:text-gray-400">
{password.length < 4 ? t('auth.password_weak') || 'Weak' : password.length < 6 ? t('auth.password_fair') || 'Fair' : password.length < 8 ? t('auth.password_good') || 'Good' : t('auth.password_strong') || 'Strong'}
</p>
</motion.div>
)}
{/* Forgot Password info */}
<AnimatePresence>
{showForgotInfo && (
@@ -859,6 +953,54 @@ export default function AuthView() {
</Card>
</motion.div>
{/* Social login buttons (visual only) */}
{mode === 'login' && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5, duration: 0.4 }}
className="mt-5"
>
<div className="flex items-center gap-3 mb-4">
<div className="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
<span className="text-xs text-gray-400 dark:text-gray-500 font-medium">{t('auth.or_continue_with') || 'or continue with'}</span>
<div className="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
</div>
<div className="grid grid-cols-3 gap-3">
<motion.button
type="button"
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
className="social-btn flex items-center justify-center gap-2 h-11 rounded-xl border border-gray-200 dark:border-gray-700 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm text-gray-700 dark:text-gray-300 font-medium text-xs hover:border-gray-300 dark:hover:border-gray-600"
onClick={() => toast.info(t('auth.social_coming_soon') || 'Social login coming soon')}
>
<Chrome className="h-4 w-4 text-blue-500" />
<span className="hidden sm:inline">Google</span>
</motion.button>
<motion.button
type="button"
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
className="social-btn flex items-center justify-center gap-2 h-11 rounded-xl border border-gray-200 dark:border-gray-700 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm text-gray-700 dark:text-gray-300 font-medium text-xs hover:border-gray-300 dark:hover:border-gray-600"
onClick={() => toast.info(t('auth.social_coming_soon') || 'Social login coming soon')}
>
<Github className="h-4 w-4" />
<span className="hidden sm:inline">GitHub</span>
</motion.button>
<motion.button
type="button"
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
className="social-btn flex items-center justify-center gap-2 h-11 rounded-xl border border-gray-200 dark:border-gray-700 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm text-gray-700 dark:text-gray-300 font-medium text-xs hover:border-gray-300 dark:hover:border-gray-600"
onClick={() => toast.info(t('auth.social_coming_soon') || 'Social login coming soon')}
>
<Globe className="h-4 w-4 text-emerald-500" />
<span className="hidden sm:inline">Moodle</span>
</motion.button>
</div>
</motion.div>
)}
{/* Features section below form */}
<FeaturesSection />
@@ -873,6 +1015,87 @@ export default function AuthView() {
<p className="text-xs text-emerald-600/40 dark:text-emerald-400/30">{t('app.tagline')}</p>
</motion.div>
</motion.div>
{/* Forgot Password Modal */}
<Dialog open={showForgotModal} onOpenChange={setShowForgotModal}>
<DialogContent className="sm:max-w-md glass-card rounded-2xl border-0">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-lg">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-emerald-400 to-teal-500 text-white shadow-sm">
<LockKeyhole className="h-4 w-4" />
</div>
{t('auth.forgot_password')}
</DialogTitle>
<DialogDescription className="text-sm text-gray-500 dark:text-gray-400 mt-2">
{t('auth.forgot_password_desc') || 'Enter your email address and we will send you a link to reset your password.'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
{forgotSent ? (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: 'spring', stiffness: 200, damping: 20 }}
className="flex flex-col items-center py-4"
>
<div className="flex items-center justify-center w-14 h-14 rounded-full bg-emerald-100 dark:bg-emerald-900/30 mb-4">
<CheckCircle2 className="h-7 w-7 text-emerald-500" />
</div>
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1">{t('auth.forgot_email_sent') || 'Reset link sent!'}</p>
<p className="text-xs text-gray-500 dark:text-gray-400 text-center">{t('auth.forgot_check_inbox') || 'Check your inbox for the password reset link.'}</p>
</motion.div>
) : (
<>
<div className="relative">
<Mail className="absolute left-3.5 top-1/2 -translate-y-1/2 h-5 w-5 text-emerald-400" />
<Input
value={forgotEmail}
onChange={(e) => setForgotEmail(e.target.value)}
className="pl-10 h-12 border-emerald-200/50 dark:border-emerald-900/30 focus:border-emerald-400 focus:ring-emerald-400/20 bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm"
placeholder="name@schule.de"
type="email"
/>
</div>
{/* Demo credentials reminder */}
<div className="flex items-start gap-2 px-3 py-3 rounded-xl bg-emerald-50/50 dark:bg-emerald-900/10 border border-emerald-200/30 dark:border-emerald-900/20 backdrop-blur-sm">
<HelpCircle className="h-4 w-4 text-emerald-500 shrink-0 mt-0.5" />
<p className="text-xs text-emerald-700 dark:text-emerald-300">
{t('polish.demo_email')}: demo@competencetrack.org / {t('polish.demo_password')}: Demo2025!
</p>
</div>
</>
)}
</div>
<DialogFooter className="flex-row gap-2 sm:justify-end">
{forgotSent ? (
<Button
onClick={() => { setShowForgotModal(false); setForgotSent(false); setForgotEmail(''); }}
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-5 font-semibold"
>
{t('auth.back_to_login') || 'Back to login'}
</Button>
) : (
<>
<Button
variant="outline"
onClick={() => setShowForgotModal(false)}
className="min-h-[44px] px-4"
>
{t('auth.cancel') || 'Cancel'}
</Button>
<Button
onClick={() => { setForgotSent(true); toast.success(t('auth.forgot_email_sent') || 'Reset link sent!'); }}
disabled={!forgotEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(forgotEmail)}
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-5 font-semibold"
>
{t('auth.send_reset_link') || 'Send reset link'}
<ArrowRight className="h-4 w-4 ml-1.5" />
</Button>
</>
)}
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
+174 -69
View File
@@ -1868,80 +1868,86 @@ export default function DashboardView() {
>
{/* ===== WELCOME HEADER ===== */}
<motion.div variants={itemVariants}>
<div className="flex flex-col gap-4">
{/* Top row: welcome + date */}
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="flex items-center gap-3.5">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: 'spring', stiffness: 150, damping: 12 }}
className="flex items-center justify-center w-12 h-12 rounded-2xl bg-gradient-to-br from-emerald-400 to-teal-500 text-white shadow-lg shadow-emerald-300/30 ring-2 ring-emerald-200/30 dark:ring-emerald-800/20"
>
<Sparkles className="w-6 h-6" />
</motion.div>
<div>
<h2 className="text-2xl font-bold bg-gradient-to-r from-emerald-600 via-teal-600 to-emerald-500 dark:from-emerald-400 dark:via-teal-400 dark:to-emerald-300 bg-clip-text text-transparent">
{getTimeGreeting()}, {currentUser?.firstName}!
</h2>
<p className="text-emerald-600/60 dark:text-emerald-400/40 mt-0.5 text-sm">{t('dashboard.overview')}</p>
{/* Animated gradient header banner */}
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-r from-emerald-500 via-teal-500 to-emerald-600 dark:from-emerald-700 dark:via-teal-700 dark:to-emerald-800 shadow-lg shadow-emerald-300/30 dark:shadow-emerald-900/40 animate-header-gradient">
{/* Decorative pattern overlay */}
<div className="absolute inset-0 pattern-dots opacity-10 pointer-events-none" />
{/* Decorative floating shapes */}
<div className="absolute top-2 right-12 w-24 h-24 rounded-full bg-white/5 animate-float-slow pointer-events-none" />
<div className="absolute bottom-0 right-40 w-16 h-16 rounded-full bg-white/5 animate-float pointer-events-none" />
<div className="absolute top-4 right-72 w-10 h-10 rounded-full bg-white/5 animate-float-reverse pointer-events-none" />
<div className="relative p-5 sm:p-6 flex flex-col sm:flex-row items-start sm:items-center gap-4">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: 'spring', stiffness: 150, damping: 12 }}
className="flex items-center justify-center w-14 h-14 rounded-2xl bg-white/20 backdrop-blur-sm text-white shadow-lg shrink-0 ring-1 ring-white/20"
>
<Sparkles className="w-7 h-7" />
</motion.div>
<div className="min-w-0 flex-1">
<h2 className="text-2xl font-bold text-white leading-tight">
{getTimeGreeting()}, {currentUser?.firstName}!
</h2>
<p className="text-emerald-100/90 mt-0.5 text-sm">{t('dashboard.overview')}</p>
<div className="flex flex-wrap items-center gap-3 mt-2">
<div className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-white/15 backdrop-blur-sm text-xs font-medium text-white/90 ring-1 ring-white/10">
<CalendarDays className="h-3.5 w-3.5" />
<span className="font-semibold">
{new Date().toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-GB', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
</div>
<div className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-white/10 backdrop-blur-sm text-xs text-white/80 ring-1 ring-white/10">
<Clock className="h-3 w-3" />
<span>{currentTime}</span>
</div>
</div>
</div>
<div className="flex flex-col items-end gap-1.5">
<div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-gradient-to-r from-emerald-50 to-teal-50/50 dark:from-emerald-900/20 dark:to-teal-900/10 border border-emerald-200/40 dark:border-emerald-900/30 text-xs font-medium text-emerald-700 dark:text-emerald-300 shadow-sm">
<CalendarDays className="h-3.5 w-3.5" />
<span className="hidden sm:inline">{t('polish.today_date')}: </span>
<span className="font-semibold">
{new Date().toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-GB', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
</div>
<div className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-white/60 dark:bg-gray-800/40 border border-emerald-100/50 dark:border-emerald-900/20 text-[11px] text-gray-500 dark:text-gray-400">
<Clock className="h-3 w-3 text-emerald-500/70" />
<span>{t('polish.last_login')}: </span>
<span className="font-medium text-gray-700 dark:text-gray-300">
{new Date().toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-GB', {
day: '2-digit',
month: 'short',
})}{' '}
{new Date().toLocaleTimeString(locale === 'de' ? 'de-DE' : 'en-GB', {
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
<div className="hidden sm:flex flex-col items-end gap-1.5 shrink-0">
<Button
onClick={() => setCurrentView('progress')}
className="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"
>
<PenLine className="h-4 w-4 mr-2" />
{t('action.log_entry')}
</Button>
</div>
</div>
{/* Tip of the day — enhanced with gradient border */}
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.4 }}
className="relative"
>
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-amber-400/20 via-emerald-400/20 to-teal-400/20 dark:from-amber-400/10 dark:via-emerald-400/10 dark:to-teal-400/10 blur-sm" />
<div className="relative flex items-center gap-3 px-4 py-3 rounded-xl bg-gradient-to-r from-emerald-50/80 via-teal-50/40 to-transparent dark:from-emerald-900/15 dark:via-teal-900/10 dark:to-transparent border border-emerald-200/30 dark:border-emerald-800/20">
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity, repeatDelay: 5 }}
className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-amber-400 to-amber-500 text-white shadow-sm shrink-0"
>
<dailyTip.icon className="h-4 w-4" />
</motion.div>
<p className="text-sm text-gray-600 dark:text-gray-300 leading-snug">
<span className="font-semibold text-emerald-700 dark:text-emerald-300">{t('dashboard.daily_tip')}:</span>{' '}
{dailyTip.text}
</p>
</div>
</motion.div>
</div>
</motion.div>
{/* Tip of the day — enhanced with gradient border */}
<motion.div variants={itemVariants}>
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.4 }}
className="relative"
>
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-amber-400/20 via-emerald-400/20 to-teal-400/20 dark:from-amber-400/10 dark:via-emerald-400/10 dark:to-teal-400/10 blur-sm" />
<div className="relative flex items-center gap-3 px-4 py-3 rounded-xl bg-gradient-to-r from-emerald-50/80 via-teal-50/40 to-transparent dark:from-emerald-900/15 dark:via-teal-900/10 dark:to-transparent border border-emerald-200/30 dark:border-emerald-800/20">
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity, repeatDelay: 5 }}
className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-amber-400 to-amber-500 text-white shadow-sm shrink-0"
>
<dailyTip.icon className="h-4 w-4" />
</motion.div>
<p className="text-sm text-gray-600 dark:text-gray-300 leading-snug">
<span className="font-semibold text-emerald-700 dark:text-emerald-300">{t('dashboard.daily_tip')}:</span>{' '}
{dailyTip.text}
</p>
</div>
</motion.div>
</motion.div>
{/* ===== QUICK STATS SUMMARY ===== */}
<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">
@@ -2041,7 +2047,7 @@ export default function DashboardView() {
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<Card className={`card-shadow-transition border-0 shadow-sm rounded-xl overflow-hidden transition-all duration-300 ${colors.hoverShadow} ${colors.glowColor} cursor-default ring-1 ring-black/[0.03] dark:ring-white/[0.05] hover:ring-2 hover:ring-emerald-200/60 dark:hover:ring-emerald-700/40`}>
<Card className={`card-shadow-transition border-0 shadow-sm rounded-xl overflow-hidden transition-all duration-300 ${colors.hoverShadow} ${colors.glowColor} cursor-default glass-card hover-lift`}>
{/* Gradient top border */}
<div className={`h-1 bg-gradient-to-r ${colors.gradient}`} />
<CardContent className={`p-5 ${colors.bg}`}>
@@ -2617,6 +2623,105 @@ export default function DashboardView() {
<div className="flex-1 h-px bg-gradient-to-r from-emerald-200/60 dark:from-emerald-800/30 to-transparent" />
</motion.div>
{/* ===== QUICK LINKS SECTION ===== */}
<motion.div variants={itemVariants}>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
{[
{ icon: BookOpen, label: t('nav.classes'), view: 'classes' as ViewName, color: 'from-emerald-400 to-teal-500', bg: 'bg-emerald-50 dark:bg-emerald-900/20', border: 'border-emerald-200/40 dark:border-emerald-900/30' },
{ icon: Flower2, label: t('nav.flower'), view: 'flower' as ViewName, color: 'from-teal-400 to-emerald-500', bg: 'bg-teal-50 dark:bg-teal-900/20', border: 'border-teal-200/40 dark:border-teal-900/30' },
{ icon: ClipboardCheck, label: t('nav.assessments'), view: 'assessments' as ViewName, color: 'from-amber-400 to-orange-500', bg: 'bg-amber-50 dark:bg-amber-900/20', border: 'border-amber-200/40 dark:border-amber-900/30' },
{ icon: BarChart3, label: t('nav.grading'), view: 'grading' as ViewName, color: 'from-violet-400 to-purple-500', bg: 'bg-violet-50 dark:bg-violet-900/20', border: 'border-violet-200/40 dark:border-violet-900/30' },
{ icon: FileText, label: t('nav.reports'), view: 'reports' as ViewName, color: 'from-rose-400 to-pink-500', bg: 'bg-rose-50 dark:bg-rose-900/20', border: 'border-rose-200/40 dark:border-rose-900/30' },
{ icon: UserCheck, label: t('nav.attendance'), view: 'attendance' as ViewName, color: 'from-teal-400 to-cyan-500', bg: 'bg-teal-50 dark:bg-teal-900/20', border: 'border-teal-200/40 dark:border-teal-900/30' },
].map((link, i) => (
<motion.button
key={link.label}
onClick={() => setCurrentView(link.view)}
whileHover={{ scale: 1.05, y: -3 }}
whileTap={{ scale: 0.97 }}
transition={{ duration: 0.18, ease: 'easeOut' }}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
className={`group flex flex-col items-center gap-2.5 p-4 rounded-xl ${link.bg} border ${link.border} hover-lift transition-shadow duration-200 hover:shadow-md`}
>
<div className={`flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br ${link.color} text-white shadow-sm transition-transform duration-200 group-hover:scale-110 group-hover:rotate-3`}>
<link.icon className="h-5 w-5" />
</div>
<span className="text-xs font-semibold text-gray-700 dark:text-gray-300 text-center leading-tight">{link.label}</span>
</motion.button>
))}
</div>
</motion.div>
{/* ===== TODAY'S SCHEDULE MINI-CALENDAR ===== */}
<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] glass-card">
<CardHeader className="pb-2 pt-5">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-bold flex items-center gap-2">
<div className="flex items-center justify-center w-7 h-7 rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 text-white shadow-sm">
<CalendarDays className="h-3.5 w-3.5" />
</div>
{locale === 'de' ? 'Heutiger Stundenplan' : "Today's Schedule"}
</CardTitle>
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
{new Date().toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-GB', { weekday: 'short', day: 'numeric', month: 'short' })}
</Badge>
</div>
</CardHeader>
<CardContent className="pb-4">
<div className="space-y-1.5">
{[
{ time: '08:00', subject: t('student.notebook_math'), room: 'R-101', status: 'completed', color: 'from-amber-400 to-orange-500' },
{ time: '09:00', subject: t('student.notebook_german'), room: 'R-203', status: 'completed', color: 'from-emerald-400 to-teal-500' },
{ time: '10:00', subject: t('student.notebook_english'), room: 'R-105', status: 'current', color: 'from-violet-400 to-purple-500' },
{ time: '11:00', subject: t('student.notebook_science'), room: 'Lab-1', status: 'upcoming', color: 'from-teal-400 to-cyan-500' },
{ time: '12:00', subject: locale === 'de' ? 'Sport' : 'PE', room: 'Gym', status: 'upcoming', color: 'from-rose-400 to-pink-500' },
].map((slot, i) => (
<motion.div
key={slot.time}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05, duration: 0.25 }}
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200 ${
slot.status === 'current'
? 'bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200/40 dark:border-emerald-800/30 shadow-sm'
: slot.status === 'completed'
? 'bg-gray-50/50 dark:bg-gray-800/20 border border-transparent'
: 'bg-white/50 dark:bg-gray-900/20 border border-transparent hover:bg-gray-50 dark:hover:bg-gray-800/30'
}`}
>
<span className={`text-xs font-mono font-semibold w-12 shrink-0 ${
slot.status === 'current' ? 'text-emerald-600 dark:text-emerald-400' : 'text-gray-400 dark:text-gray-500'
}`}>
{slot.time}
</span>
<div className={`flex items-center justify-center w-7 h-7 rounded-lg bg-gradient-to-br ${slot.color} text-white shadow-sm shrink-0 ${slot.status === 'completed' ? 'opacity-60' : ''}`}>
<BookOpen className="h-3.5 w-3.5" />
</div>
<div className="min-w-0 flex-1">
<p className={`text-xs font-semibold truncate ${slot.status === 'completed' ? 'text-gray-400 dark:text-gray-500' : 'text-gray-800 dark:text-gray-200'}`}>
{slot.subject}
</p>
<p className="text-[10px] text-gray-400 dark:text-gray-500">{slot.room}</p>
</div>
{slot.status === 'current' && (
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 1.5, repeat: Infinity }}
className="w-2 h-2 rounded-full bg-emerald-500 shrink-0"
/>
)}
{slot.status === 'completed' && (
<CheckCircle2 className="h-4 w-4 text-gray-300 dark:text-gray-600 shrink-0" />
)}
</motion.div>
))}
</div>
</CardContent>
</Card>
</motion.div>
{/* ===== CLASS OVERVIEW MINI CARD GRID ===== */}
<motion.div variants={itemVariants}>
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-emerald-500 overflow-hidden ring-1 ring-black/[0.03] dark:ring-white/[0.05]">
@@ -3036,7 +3141,7 @@ export default function DashboardView() {
</p>
</div>
{!n.read && (
<Badge className="bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300 text-xs shrink-0 shadow-sm">
<Badge className="bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300 text-xs shrink-0 shadow-sm animate-badge-pulse">
{locale === 'de' ? 'Neu' : 'New'}
</Badge>
)}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff