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:
1 parent
7ea8dc48c8
commit
a159ed7fe6
22 files changed
+8349
-153
No files matched your search
@@ -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 />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user