Round 25: Substitute Teacher, School Newsletter, Career Guidance, Styling

- Substitute Teacher Management View: pool, absences, assignments, statistics, weekly schedule
- School Newsletter View: editor, templates, distribution, analytics, open/click tracking
- Student Career & Guidance View: career exploration, interest quiz, portfolio, goals, appointments
- 6 new Prisma models: SubstituteTeacher, TeacherAbsence, SubstitutionAssignment, CareerProfile, CareerGoal, CareerAppointment
- Enhanced Newsletter model with content, scheduling, and analytics fields
- Enhanced settings-view with gradient header, glassmorphism cards, danger zone
- Enhanced student-portal-view with animated welcome banner, quick action cards
- 16+ 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 06:36:34 +00:00
1 parent a159ed7fe6
commit 3d0d518bb9
57 files changed
+8370 -48

No files matched your search

+17
View File
@@ -70,6 +70,8 @@ import {
BookOpen as BookOpenIconNav,
Database,
PartyPopper,
Newspaper,
Compass,
} from 'lucide-react';
import {
Sidebar,
@@ -187,6 +189,9 @@ 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';
import StudentCareerView from './student-career-view';
import SubstituteTeacherView from './substitute-teacher-view';
import SchoolNewsletterView from './school-newsletter-view';
// Dynamic imports for heavy components with loading skeletons
const AnalyticsView = dynamic(() => import('./analytics-view'), {
@@ -257,9 +262,12 @@ const navSections: NavSection[] = [
{ 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: 'student-career', icon: Compass, labelKey: 'nav.student-career' },
{ 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: 'school-newsletter', icon: Newspaper, labelKey: 'nav.school-newsletter' },
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
{ key: 'data-import-export', icon: Database, labelKey: 'nav.data-import-export' },
],
},
@@ -302,9 +310,12 @@ const studentNavSections: NavSection[] = [
{ 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: 'student-career', icon: Compass, labelKey: 'nav.student-career' },
{ 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: 'school-newsletter', icon: Newspaper, labelKey: 'nav.school-newsletter' },
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
{ key: 'notification-center', icon: Bell, labelKey: 'nav.notification_center' },
{ key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' },
{ key: 'settings', icon: Settings, labelKey: 'nav.settings' },
@@ -329,9 +340,12 @@ const parentNavSections: NavSection[] = [
{ key: 'communication', icon: MessageSquare, labelKey: 'nav.communication' },
{ key: 'student-achievements', icon: Trophy, labelKey: 'achievements.title' },
{ key: 'student-study-planner', icon: Timer, labelKey: 'study_planner.title' },
{ key: 'student-career', icon: Compass, labelKey: 'nav.student-career' },
{ 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: 'school-newsletter', icon: Newspaper, labelKey: 'nav.school-newsletter' },
{ key: 'substitute-teacher', icon: UserCheck, labelKey: 'nav.substitute-teacher' },
{ key: 'notification-center', icon: Bell, labelKey: 'nav.notification_center' },
{ key: 'announcements', icon: Megaphone, labelKey: 'nav.announcements' },
],
@@ -483,8 +497,11 @@ function renderView(view: ViewName) {
case 'school-library': return <SchoolLibraryView />;
case 'report-cards': return <ReportCardView />;
case 'student-wellness': return <StudentWellnessView />;
case 'student-career': return <StudentCareerView />;
case 'data-import-export': return <DataImportExportView />;
case 'school-events': return <SchoolEventsView />;
case 'substitute-teacher': return <SubstituteTeacherView />;
case 'school-newsletter': return <SchoolNewsletterView />;
default: return <DashboardView />;
}
}
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
File diff suppressed because it is too large. Load diff
View File
File mode changed.
+129 -27
View File
@@ -1,7 +1,7 @@
'use client';
import React, { useEffect, useState, useCallback } from 'react';
import { motion } from 'framer-motion';
import { motion, AnimatePresence } from 'framer-motion';
import {
Settings as SettingsIcon,
School,
@@ -105,6 +105,7 @@ import { Separator } from '@/components/ui/separator';
import { Skeleton } from '@/components/ui/skeleton';
import { Checkbox } from '@/components/ui/checkbox';
import { Progress } from '@/components/ui/progress';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { useAppStore } from '@/lib/store';
import { t } from '@/lib/i18n';
import { apiGet, apiPost, apiPut, apiDelete } from '@/lib/api';
@@ -2576,20 +2577,54 @@ export default function SettingsView() {
animate="visible"
className="space-y-6"
>
{/* Header */}
{/* Header — Gradient Banner with School Branding */}
<motion.div variants={itemVariants}>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-400 to-teal-500 text-white shadow-md shadow-emerald-300/30">
<SettingsIcon className="w-5 h-5" />
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-emerald-500 via-teal-500 to-cyan-500 dark:from-emerald-700 dark:via-teal-700 dark:to-cyan-700 p-6 sm:p-8 text-white shadow-xl shadow-emerald-200/30 dark:shadow-emerald-900/30">
{/* Decorative elements */}
<div className="absolute top-0 right-0 w-64 h-64 bg-white/5 rounded-full -translate-y-1/2 translate-x-1/3" />
<div className="absolute bottom-0 left-0 w-48 h-48 bg-white/5 rounded-full translate-y-1/2 -translate-x-1/4" />
<div className="absolute top-4 right-4 sm:top-6 sm:right-6">
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 4, repeat: Infinity, ease: 'easeInOut' }}
>
<SettingsIcon className="h-8 w-8 text-white/30" />
</motion.div>
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">{t('settings.title')}</h2>
<p className="text-emerald-600/60 dark:text-emerald-400/40 mt-0.5">{t('app.subtitle')}</p>
<div className="relative z-10 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-4">
<div className="flex items-center justify-center w-14 h-14 rounded-2xl bg-white/20 backdrop-blur-sm ring-2 ring-white/20 shadow-lg">
<SettingsIcon className="w-7 h-7" />
</div>
<div>
<h2 className="text-2xl sm:text-3xl font-bold">{t('settings.title')}</h2>
<p className="text-emerald-100/80 mt-0.5 text-sm sm:text-base">{selectedSchool?.name || t('app.subtitle')}</p>
</div>
</div>
<div className="flex items-center gap-3">
{/* User Profile Avatar */}
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/15 backdrop-blur-sm border border-white/20">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-white/25 text-white text-xs font-bold">
{currentUser?.firstName?.[0] || ''}{currentUser?.lastName?.[0] || ''}
</div>
<div className="hidden sm:block">
<p className="text-xs font-medium text-white">{currentUser?.firstName} {currentUser?.lastName}</p>
<p className="text-[10px] text-white/70">{currentUser?.role === 'SUPER_ADMIN' ? 'Super Admin' : currentUser?.role === 'SCHOOL_ADMIN' ? 'School Admin' : 'Admin'}</p>
</div>
</div>
<div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-white/15 backdrop-blur-sm border border-white/20 text-sm font-medium">
<CalendarDays className="h-3.5 w-3.5" />
<span className="font-semibold">
{new Date().toLocaleDateString(undefined, { weekday: 'short', day: 'numeric', month: 'short' })}
</span>
</div>
</div>
</div>
</div>
</motion.div>
{/* Quick stats banner */}
{/* Quick stats banner — with hover lift and gradient borders */}
<motion.div variants={itemVariants}>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3">
{[
@@ -2598,8 +2633,14 @@ export default function SettingsView() {
{ icon: BookOpen, label: t('polish.total_subjects'), value: subjects.length, color: 'from-amber-50 to-amber-100/50 dark:from-amber-900/20 dark:to-amber-800/10', text: 'text-amber-700 dark:text-amber-300', iconBg: 'bg-gradient-to-br from-amber-400 to-amber-500', border: 'border-amber-200/40 dark:border-amber-900/30', tab: 'subjects' },
{ icon: Calendar, label: t('polish.total_years'), value: schoolYears.length, color: 'from-teal-50 to-teal-100/50 dark:from-teal-900/20 dark:to-teal-800/10', text: 'text-teal-700 dark:text-teal-300', iconBg: 'bg-gradient-to-br from-teal-400 to-emerald-500', border: 'border-teal-200/40 dark:border-teal-900/30', tab: 'years' },
{ icon: Activity, label: t('settings.tab_audit'), value: auditEntries.length, color: 'from-slate-50 to-slate-100/50 dark:from-slate-800/40 dark:to-slate-700/10', text: 'text-slate-700 dark:text-slate-300', iconBg: 'bg-gradient-to-br from-slate-400 to-slate-500', border: 'border-slate-200/40 dark:border-slate-700/30', tab: 'audit' },
].map((stat) => (
<div key={stat.label} className={`p-3 rounded-xl bg-gradient-to-br ${stat.color} border ${stat.border} flex items-center gap-2.5 cursor-pointer hover:shadow-md transition-shadow`} onClick={() => setActiveTab(stat.tab)}>
].map((stat, idx) => (
<motion.div
key={stat.label}
whileHover={{ y: -2, boxShadow: '0 8px 24px -6px rgba(16, 185, 129, 0.18)' }}
transition={{ duration: 0.2 }}
className={`p-3 rounded-xl bg-gradient-to-br ${stat.color} border ${stat.border} flex items-center gap-2.5 cursor-pointer transition-shadow`}
onClick={() => setActiveTab(stat.tab)}
>
<div className={`flex items-center justify-center w-9 h-9 rounded-lg ${stat.iconBg} text-white shadow-sm shrink-0`}>
<stat.icon className="h-4 w-4" />
</div>
@@ -2607,15 +2648,15 @@ export default function SettingsView() {
<p className="text-[10px] uppercase tracking-wide font-semibold text-gray-500 dark:text-gray-400 truncate">{stat.label}</p>
<p className={`text-xl font-bold ${stat.text}`}>{stat.value}</p>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
{/* Tabs */}
{/* Tabs — with smooth framer-motion transitions */}
<motion.div variants={itemVariants}>
<Tabs value={activeTab} onValueChange={handleTabChange}>
<TabsList className="bg-emerald-50 dark:bg-emerald-900/20 rounded-xl flex flex-wrap gap-1 h-auto p-1">
<TabsList className="bg-emerald-50/80 dark:bg-emerald-900/20 backdrop-blur-sm rounded-xl flex flex-wrap gap-1 h-auto p-1 ring-1 ring-emerald-200/30 dark:ring-emerald-800/20">
<TabsTrigger value="school" className="rounded-lg data-[state=active]:bg-emerald-500 data-[state=active]:text-white">
<School className="h-4 w-4 mr-1.5" />
<span className="hidden sm:inline">{t('settings.tab_school')}</span>
@@ -2688,7 +2729,15 @@ export default function SettingsView() {
{/* ── School Info Tab ─────────────────────────────────── */}
<TabsContent value="school">
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-emerald-500 overflow-hidden">
<AnimatePresence mode="wait">
<motion.div
key="school"
initial={{ opacity: 0, x: 12 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -12 }}
transition={{ duration: 0.25, ease: 'easeOut' }}
>
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl border-l-3 border-l-emerald-500 overflow-hidden">
<CardHeader className="bg-gradient-to-r from-emerald-50/50 to-transparent dark:from-emerald-900/10 dark:to-transparent">
<CardTitle className="flex items-center gap-2">
<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">
@@ -2778,11 +2827,21 @@ export default function SettingsView() {
<NotificationSoundSetting />
</div>
</Card>
</motion.div>
</AnimatePresence>
</TabsContent>
{/* ── School Years Tab ────────────────────────────────── */}
<TabsContent value="years">
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-teal-500 overflow-hidden">
<AnimatePresence mode="wait">
<motion.div
key="years"
initial={{ opacity: 0, x: 12 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -12 }}
transition={{ duration: 0.25, ease: 'easeOut' }}
>
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl border-l-3 border-l-teal-500 overflow-hidden">
<CardHeader className="bg-gradient-to-r from-teal-50/50 to-transparent dark:from-teal-900/10 dark:to-transparent">
<CardTitle className="flex items-center gap-2">
<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">
@@ -2890,6 +2949,8 @@ export default function SettingsView() {
</Dialog>
</CardContent>
</Card>
</motion.div>
</AnimatePresence>
</TabsContent>
{/* ── Subjects Tab ────────────────────────────────────── */}
@@ -3752,9 +3813,17 @@ export default function SettingsView() {
{/* ── Data Management Tab ─────────────────────────────── */}
<TabsContent value="data">
<div className="space-y-6">
<AnimatePresence mode="wait">
<motion.div
key="data"
initial={{ opacity: 0, x: 12 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -12 }}
transition={{ duration: 0.25, ease: 'easeOut' }}
className="space-y-6"
>
{/* Data Import */}
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-amber-500 overflow-hidden">
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl border-l-3 border-l-amber-500 overflow-hidden">
<CardHeader className="bg-gradient-to-r from-amber-50/50 to-transparent dark:from-amber-900/10 dark:to-transparent">
<CardTitle className="flex items-center gap-2">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400">
@@ -3885,7 +3954,7 @@ export default function SettingsView() {
</Card>
{/* Enhanced Data Export */}
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-emerald-500 overflow-hidden">
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl border-l-3 border-l-emerald-500 overflow-hidden">
<CardHeader className="bg-gradient-to-r from-emerald-50/50 to-transparent dark:from-emerald-900/10 dark:to-transparent">
<CardTitle className="flex items-center gap-2">
<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">
@@ -3988,7 +4057,7 @@ export default function SettingsView() {
</Card>
{/* GDPR Data Erasure — Danger Zone */}
<div className="danger-zone">
<div className="danger-zone animate-pulse-glow">
<div className="flex items-center gap-2 mb-3">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-rose-100 dark:bg-rose-900/30 text-rose-600 dark:text-rose-400">
<AlertTriangle className="h-4 w-4" />
@@ -4047,7 +4116,8 @@ export default function SettingsView() {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</motion.div>
</AnimatePresence>
</TabsContent>
{/* ── Demo Accounts Tab ───────────────────────────────── */}
@@ -4806,7 +4876,15 @@ export default function SettingsView() {
{/* ── Branding Tab ───────────────────────────────────────────── */}
<TabsContent value="branding">
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-teal-500 overflow-hidden">
<AnimatePresence mode="wait">
<motion.div
key="branding"
initial={{ opacity: 0, x: 12 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -12 }}
transition={{ duration: 0.25, ease: 'easeOut' }}
>
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl border-l-3 border-l-teal-500 overflow-hidden">
<CardHeader className="bg-gradient-to-r from-teal-50/50 to-transparent dark:from-teal-900/10 dark:to-transparent">
<CardTitle className="flex items-center gap-2">
<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">
@@ -4817,7 +4895,7 @@ export default function SettingsView() {
<CardDescription>{t('branding.colors')} · {t('branding.typography')} · {t('branding.contact')}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Logo */}
{/* Logo Preview Section */}
<div className="space-y-2">
<Label className="text-sm font-medium text-teal-700 dark:text-teal-400">{t('branding.logo_url')}</Label>
<Input
@@ -4826,15 +4904,28 @@ export default function SettingsView() {
placeholder={t('branding.logo_placeholder')}
className="border-teal-200 dark:border-teal-900/30 rounded-xl"
/>
{brandingForm.logoUrl && (
<div className="mt-2 flex items-center gap-3">
{brandingForm.logoUrl ? (
<div className="mt-2 flex items-center gap-3 p-3 rounded-xl bg-gradient-to-r from-teal-50/50 to-emerald-50/30 dark:from-teal-900/10 dark:to-emerald-900/5 border border-teal-200/30 dark:border-teal-900/20">
<img src={brandingForm.logoUrl} alt="Logo preview" className="w-12 h-12 rounded-xl object-contain border border-teal-200 dark:border-teal-900/30" onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
<span className="text-xs text-gray-500 dark:text-gray-400">{t('branding.preview')}</span>
<div>
<p className="text-xs font-semibold text-teal-700 dark:text-teal-300">{t('branding.preview')}</p>
<p className="text-[10px] text-gray-500 dark:text-gray-400">{selectedSchool?.name}</p>
</div>
</div>
) : (
<div className="mt-2 flex items-center gap-3 p-3 rounded-xl bg-gradient-to-r from-teal-50/50 to-emerald-50/30 dark:from-teal-900/10 dark:to-emerald-900/5 border border-teal-200/30 dark:border-teal-900/20">
<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-sm">
<School className="h-6 w-6" />
</div>
<div>
<p className="text-xs font-semibold text-teal-700 dark:text-teal-300">{selectedSchool?.name || 'School'}</p>
<p className="text-[10px] text-gray-500 dark:text-gray-400">{t('branding.logo_placeholder')}</p>
</div>
</div>
)}
</div>
{/* Colors */}
{/* Colors — with visual color picker */}
<div className="space-y-4">
<p className="text-sm font-semibold text-teal-700 dark:text-teal-400">{t('branding.colors')}</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
@@ -4887,6 +4978,15 @@ export default function SettingsView() {
</div>
</div>
</div>
{/* Color preview strip */}
<div className="flex items-center gap-2 p-3 rounded-xl bg-gray-50/50 dark:bg-gray-800/50 border border-gray-200/30 dark:border-gray-700/20">
<span className="text-xs text-gray-500 dark:text-gray-400 shrink-0">Preview:</span>
<div className="flex items-center gap-1.5 flex-1">
<div className="h-8 flex-1 rounded-lg" style={{ backgroundColor: brandingForm.primaryColor }} />
<div className="h-8 flex-1 rounded-lg" style={{ backgroundColor: brandingForm.secondaryColor }} />
<div className="h-8 flex-1 rounded-lg" style={{ backgroundColor: brandingForm.accentColor }} />
</div>
</div>
</div>
{/* Typography */}
@@ -5038,6 +5138,8 @@ export default function SettingsView() {
</div>
</CardContent>
</Card>
</motion.div>
</AnimatePresence>
</TabsContent>
{/* ── Email Tab ───────────────────────────────────────────────── */}
File diff suppressed because it is too large. Load diff
+143 -14
View File
@@ -594,7 +594,7 @@ export default function StudentPortalView() {
>
{/* ── Hero Section ──────────────────────────────────────────────── */}
<motion.div variants={itemVariants}>
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-emerald-500 via-teal-500 to-cyan-500 dark:from-emerald-700 dark:via-teal-700 dark:to-cyan-700 p-6 sm:p-8 text-white shadow-xl shadow-emerald-200/30 dark:shadow-emerald-900/30">
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-emerald-500 via-teal-500 to-cyan-500 dark:from-emerald-700 dark:via-teal-700 dark:to-cyan-700 p-6 sm:p-8 text-white shadow-xl shadow-emerald-200/30 dark:shadow-emerald-900/30 animate-pulse-glow">
{/* Decorative elements */}
<div className="absolute top-0 right-0 w-64 h-64 bg-white/5 rounded-full -translate-y-1/2 translate-x-1/3" />
<div className="absolute bottom-0 left-0 w-48 h-48 bg-white/5 rounded-full translate-y-1/2 -translate-x-1/4" />
@@ -636,7 +636,7 @@ export default function StudentPortalView() {
</div>
</div>
{/* Quick Stats Bar inside hero */}
{/* Quick Stats Bar inside hero — with gradient borders */}
<div className="relative z-10 mt-6 grid grid-cols-2 sm:grid-cols-4 gap-3">
{[
{ label: t('student_portal.points_earned'), value: totalPoints, icon: Zap, color: 'bg-white/15' },
@@ -647,7 +647,7 @@ export default function StudentPortalView() {
<motion.div
key={stat.label}
whileHover={{ scale: 1.04, backgroundColor: 'rgba(255,255,255,0.25)' }}
className={`flex items-center gap-3 ${stat.color} backdrop-blur-sm rounded-xl px-3 py-2.5 border border-white/10 transition-colors cursor-default`}
className={`flex items-center gap-3 ${stat.color} backdrop-blur-sm rounded-xl px-3 py-2.5 border border-white/10 transition-colors cursor-default gradient-border-card`}
>
<stat.icon className="h-5 w-5 text-white/80 shrink-0" />
<div className="min-w-0">
@@ -664,7 +664,7 @@ export default function StudentPortalView() {
{/* ── Level Up Progress Bar ──────────────────────────────────────── */}
<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]">
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl overflow-hidden ring-1 ring-black/[0.03] dark:ring-white/[0.05]">
<CardContent className="p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
@@ -696,7 +696,7 @@ export default function StudentPortalView() {
{/* ── Streak Counter ──────────────────────────────────────────────── */}
<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]">
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl overflow-hidden ring-1 ring-black/[0.03] dark:ring-white/[0.05]">
<CardContent className="p-4">
<div className="flex items-center gap-4">
<div className="relative">
@@ -738,7 +738,7 @@ export default function StudentPortalView() {
</Card>
</motion.div>
{/* ── Quick Action Cards ──────────────────────────────────────────── */}
{/* ── Quick Action Cards — with hover lift effects ──────────────────── */}
<motion.div variants={itemVariants}>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[
@@ -751,11 +751,12 @@ export default function StudentPortalView() {
return (
<motion.div
key={idx}
whileHover={{ scale: 1.03, y: -3 }}
whileHover={{ scale: 1.03, y: -4 }}
whileTap={{ scale: 0.97 }}
className="card-hover-lift"
>
<Card
className="border-0 shadow-sm rounded-xl overflow-hidden cursor-pointer hover:shadow-md transition-all duration-300"
className="glassmorphism-card border-0 shadow-sm rounded-xl overflow-hidden cursor-pointer hover:shadow-lg transition-all duration-300"
onClick={() => {
const store = useAppStore.getState();
store.setCurrentView(action.view);
@@ -774,11 +775,139 @@ export default function StudentPortalView() {
</div>
</motion.div>
{/* ── Today's Schedule Mini-Timeline & Motivational Quote ─────────── */}
<motion.div variants={itemVariants}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Today's Schedule Mini-Timeline */}
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl overflow-hidden border-l-3 border-l-emerald-500">
<CardHeader className="pb-2 bg-gradient-to-r from-emerald-50/50 to-transparent dark:from-emerald-900/10 dark:to-transparent">
<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-emerald-400 to-teal-500 text-white shadow-sm">
<CalendarDays className="h-3.5 w-3.5" />
</div>
{t('student_portal.my_schedule')}
<div className="h-0.5 w-12 rounded-full bg-gradient-to-r from-emerald-400 to-transparent" />
</CardTitle>
</CardHeader>
<CardContent className="p-4">
{schedule.length > 0 ? (
<div className="space-y-2">
{schedule.slice(0, 4).map((period, idx) => {
const isCurrent = currentPeriodIndex === idx;
const isPast = currentPeriodIndex !== -1 && idx < currentPeriodIndex;
return (
<motion.div
key={idx}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.05, duration: 0.3 }}
className={`flex items-center gap-3 p-2.5 rounded-lg transition-all ${
isCurrent
? 'bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200/50 dark:border-emerald-800/30'
: isPast
? 'opacity-50'
: 'bg-gray-50/50 dark:bg-gray-800/30'
}`}
>
<div className={`flex items-center justify-center w-8 h-8 rounded-lg text-xs font-bold shrink-0 ${
isCurrent
? 'bg-gradient-to-br from-emerald-400 to-teal-500 text-white shadow-sm'
: 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400'
}`}>
{period.period}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate">{period.subject}</p>
{isCurrent && (
<span className="status-dot animate-pulse-glow" />
)}
</div>
<p className="text-[10px] text-gray-500 dark:text-gray-400">
{period.startTime} - {period.endTime}
{period.room ? ` · ${period.room}` : ''}
</p>
</div>
{isCurrent && (
<Badge className="text-[10px] bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-800">
Now
</Badge>
)}
</motion.div>
);
})}
</div>
) : (
<div className="text-center py-4">
<p className="text-xs text-gray-500 dark:text-gray-400">{t('student_portal.no_schedule')}</p>
</div>
)}
</CardContent>
</Card>
{/* Motivational Quote Section with fade animation */}
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl overflow-hidden border-l-3 border-l-violet-500">
<CardHeader className="pb-2 bg-gradient-to-r from-violet-50/50 to-transparent dark:from-violet-900/10 dark:to-transparent">
<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-violet-400 to-purple-500 text-white shadow-sm">
<Sparkles className="h-3.5 w-3.5" />
</div>
{t('student_portal.daily_inspiration')}
<div className="h-0.5 w-12 rounded-full bg-gradient-to-r from-violet-400 to-transparent" />
</CardTitle>
</CardHeader>
<CardContent className="p-4">
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
className="space-y-4"
>
<div className="p-4 rounded-xl bg-gradient-to-br from-violet-50/50 to-purple-50/30 dark:from-violet-900/10 dark:to-purple-900/5 border border-violet-100/40 dark:border-violet-900/20">
<p className="text-sm font-medium text-gray-800 dark:text-gray-200 italic leading-relaxed">
&ldquo;{locale === 'de'
? 'Bildung ist nicht das Befuellen eines Fasses, sondern das Entzuenden einer Flamme.'
: 'Education is not the filling of a pail, but the lighting of a fire.'
}&rdquo;
</p>
<p className="text-xs text-violet-600 dark:text-violet-400 mt-2 font-semibold">
— {locale === 'de' ? 'Heraklit' : 'William Butler Yeats'}
</p>
</div>
{/* My Progress section with animated progress bars */}
<div className="space-y-3">
<p className="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">{t('student_portal.my_competencies')}</p>
{competencies.slice(0, 4).map((comp, idx) => {
const colors = masteryColors[comp.level] || masteryColors[1];
return (
<div key={idx} className="space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="text-gray-700 dark:text-gray-300 font-medium truncate">{comp.subject}</span>
<span className="text-gray-500 dark:text-gray-400 font-semibold">{comp.progress}%</span>
</div>
<div className="relative h-2 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden">
<motion.div
className={`absolute inset-y-0 left-0 rounded-full bg-gradient-to-r ${colors.gradient} progress-animated`}
initial={{ width: 0 }}
animate={{ width: `${comp.progress}%` }}
transition={{ duration: 0.8, ease: 'easeOut', delay: 0.2 + idx * 0.1 }}
/>
</div>
</div>
);
})}
</div>
</motion.div>
</CardContent>
</Card>
</div>
</motion.div>
{/* ── Charts Section: Radar + Grade Trend ─────────────────────────── */}
<motion.div variants={itemVariants}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Competency Radar Chart */}
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl overflow-hidden">
<CardHeader className="pb-2">
<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-emerald-400 to-teal-500 text-white shadow-sm">
@@ -808,7 +937,7 @@ export default function StudentPortalView() {
</Card>
{/* Grade Trend Chart */}
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl overflow-hidden">
<CardHeader className="pb-2">
<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-violet-400 to-purple-500 text-white shadow-sm">
@@ -845,7 +974,7 @@ export default function StudentPortalView() {
{/* ── Weekly Streak Heatmap ───────────────────────────────────────── */}
<motion.div variants={itemVariants}>
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl overflow-hidden">
<CardHeader className="pb-2">
<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-orange-400 to-red-500 text-white shadow-sm">
@@ -900,7 +1029,7 @@ export default function StudentPortalView() {
{/* ── Daily Challenge Widget ──────────────────────────────────────── */}
<motion.div variants={itemVariants}>
<Card className="border-0 shadow-sm rounded-xl overflow-hidden border-l-3 border-l-amber-500">
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl overflow-hidden border-l-3 border-l-amber-500">
<CardHeader className="pb-2 bg-gradient-to-r from-amber-50/50 to-transparent dark:from-amber-900/10 dark:to-transparent">
<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-amber-500 text-white shadow-sm">
@@ -937,7 +1066,7 @@ export default function StudentPortalView() {
{/* ── Learning Time Tracker ───────────────────────────────────────── */}
<motion.div variants={itemVariants}>
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
<Card className="glassmorphism-card border-0 shadow-sm rounded-xl overflow-hidden">
<CardHeader className="pb-2">
<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-sky-400 to-cyan-500 text-white shadow-sm">
@@ -975,7 +1104,7 @@ export default function StudentPortalView() {
{/* ── Main Tabs ──────────────────────────────────────────────────── */}
<motion.div variants={itemVariants}>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList className="grid w-full grid-cols-3 sm:grid-cols-6 h-auto gap-1 bg-gray-100/50 dark:bg-gray-800/50 p-1 rounded-xl">
<TabsList className="grid w-full grid-cols-3 sm:grid-cols-6 h-auto gap-1 bg-gray-100/50 dark:bg-gray-800/50 backdrop-blur-sm p-1 rounded-xl ring-1 ring-gray-200/30 dark:ring-gray-700/20">
<TabsTrigger value="competencies" className="text-xs sm:text-sm gap-1.5 min-h-[40px] data-[state=active]:bg-white dark:data-[state=active]:bg-gray-900 data-[state=active]:shadow-sm rounded-lg">
<BookOpen className="h-3.5 w-3.5" />
<span className="hidden sm:inline">{t('student_portal.my_competencies')}</span>
View File
File mode changed.
File diff suppressed because it is too large. Load diff