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:
1 parent
a159ed7fe6
commit
3d0d518bb9
57 files changed
+8370
-48
No files matched your search
@@ -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 />;
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
File diff suppressed because it is too large.
Load diff
Regular → Executable
File mode changed.
@@ -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
@@ -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">
|
||||
“{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.'
|
||||
}”
|
||||
</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>
|
||||
|
||||
Regular → Executable
File mode changed.
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user