feat: styling improvements + grade trend chart + student comparison
- Enhanced reports, grading, attendance, settings views with gradient headers, hover effects, section icons - Added grade trend over time chart (AreaChart with reversed Y-axis) - Added student comparison feature in competence flower (dual radar overlay) - Added 6 new i18n keys for grade trend and comparison - Lint passes cleanly
This commit is contained in:
1 parent
0c1f739989
commit
456cdf9714
6 files changed
+394
-75
No files matched your search
@@ -259,16 +259,16 @@ function MonthlyHeatmap({ sessions }: { sessions: AttendanceSession[] }) {
|
|||||||
const data = dateMap.get(dateKey);
|
const data = dateMap.get(dateKey);
|
||||||
if (!data || data.total === 0) return 'bg-gray-100 dark:bg-gray-800';
|
if (!data || data.total === 0) return 'bg-gray-100 dark:bg-gray-800';
|
||||||
const rate = data.present / data.total;
|
const rate = data.present / data.total;
|
||||||
if (rate >= 0.9) return 'bg-emerald-500 dark:bg-emerald-600';
|
if (rate >= 0.9) return 'bg-gradient-to-br from-emerald-500 to-emerald-600 dark:from-emerald-500 dark:to-emerald-600';
|
||||||
if (rate >= 0.7) return 'bg-emerald-400 dark:bg-emerald-500';
|
if (rate >= 0.7) return 'bg-gradient-to-br from-emerald-400 to-teal-500 dark:from-emerald-500 dark:to-emerald-600';
|
||||||
if (rate >= 0.5) return 'bg-emerald-300 dark:bg-emerald-700';
|
if (rate >= 0.5) return 'bg-gradient-to-br from-emerald-300 to-emerald-400 dark:from-emerald-700 dark:to-emerald-800';
|
||||||
if (rate >= 0.3) return 'bg-emerald-200 dark:bg-emerald-800';
|
if (rate >= 0.3) return 'bg-gradient-to-br from-amber-200 to-amber-300 dark:from-emerald-800 dark:to-emerald-900';
|
||||||
return 'bg-rose-300 dark:bg-rose-700';
|
return 'bg-gradient-to-br from-rose-300 to-rose-400 dark:from-rose-700 dark:to-rose-800';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border-0 shadow-lg bg-gradient-to-br from-white to-emerald-50/30 dark:from-gray-900 dark:to-emerald-950/20">
|
<Card className="border-0 shadow-lg rounded-xl bg-gradient-to-br from-white to-emerald-50/30 dark:from-gray-900 dark:to-emerald-950/20 overflow-hidden">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2 bg-gradient-to-r from-emerald-50/50 to-teal-50/30 dark:from-emerald-900/10 dark:to-teal-900/5">
|
||||||
<CardTitle className="text-lg flex items-center gap-2">
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
<CalendarCheck className="h-5 w-5 text-emerald-500" />
|
<CalendarCheck className="h-5 w-5 text-emerald-500" />
|
||||||
{t('attendance.calendar')} — {monthNames[month]} {year}
|
{t('attendance.calendar')} — {monthNames[month]} {year}
|
||||||
@@ -290,7 +290,7 @@ function MonthlyHeatmap({ sessions }: { sessions: AttendanceSession[] }) {
|
|||||||
<div
|
<div
|
||||||
className={`aspect-square rounded-sm flex items-center justify-center text-xs
|
className={`aspect-square rounded-sm flex items-center justify-center text-xs
|
||||||
${getHeatColor(cell.day, cell.dateKey)}
|
${getHeatColor(cell.day, cell.dateKey)}
|
||||||
${cell.day ? 'hover:ring-2 hover:ring-emerald-400 transition-all cursor-default' : ''}
|
${cell.day ? 'hover:ring-2 hover:ring-emerald-400 hover:scale-110 transition-all duration-150 cursor-default' : ''}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{cell.day && (
|
{cell.day && (
|
||||||
@@ -323,11 +323,11 @@ function MonthlyHeatmap({ sessions }: { sessions: AttendanceSession[] }) {
|
|||||||
<span>{t('attendance.absent')}</span>
|
<span>{t('attendance.absent')}</span>
|
||||||
<div className="flex gap-0.5">
|
<div className="flex gap-0.5">
|
||||||
<div className="w-3 h-3 rounded-sm bg-gray-100 dark:bg-gray-800" />
|
<div className="w-3 h-3 rounded-sm bg-gray-100 dark:bg-gray-800" />
|
||||||
<div className="w-3 h-3 rounded-sm bg-rose-300 dark:bg-rose-700" />
|
<div className="w-3 h-3 rounded-sm bg-gradient-to-br from-rose-300 to-rose-400 dark:from-rose-700 dark:to-rose-800" />
|
||||||
<div className="w-3 h-3 rounded-sm bg-emerald-200 dark:bg-emerald-800" />
|
<div className="w-3 h-3 rounded-sm bg-gradient-to-br from-amber-200 to-amber-300 dark:from-emerald-800 dark:to-emerald-900" />
|
||||||
<div className="w-3 h-3 rounded-sm bg-emerald-300 dark:bg-emerald-700" />
|
<div className="w-3 h-3 rounded-sm bg-gradient-to-br from-emerald-300 to-emerald-400 dark:from-emerald-700 dark:to-emerald-800" />
|
||||||
<div className="w-3 h-3 rounded-sm bg-emerald-400 dark:bg-emerald-500" />
|
<div className="w-3 h-3 rounded-sm bg-gradient-to-br from-emerald-400 to-teal-500 dark:from-emerald-500 dark:to-emerald-600" />
|
||||||
<div className="w-3 h-3 rounded-sm bg-emerald-500 dark:bg-emerald-600" />
|
<div className="w-3 h-3 rounded-sm bg-gradient-to-br from-emerald-500 to-emerald-600 dark:from-emerald-500 dark:to-emerald-600" />
|
||||||
</div>
|
</div>
|
||||||
<span>{t('attendance.present')}</span>
|
<span>{t('attendance.present')}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,14 +401,15 @@ function StatsCards({ sessions }: { sessions: AttendanceSession[] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||||
{cards.map((card) => (
|
{cards.map((card, idx) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={card.label}
|
key={card.label}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3, delay: idx * 0.06 }}
|
||||||
|
whileHover={{ y: -2 }}
|
||||||
>
|
>
|
||||||
<Card className={`border-2 ${card.border} shadow-md hover:shadow-lg transition-shadow`}>
|
<Card className={`border-2 ${card.border} shadow-md hover:shadow-lg transition-shadow rounded-xl overflow-hidden`}>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<card.icon className="w-4 h-4" />
|
<card.icon className="w-4 h-4" />
|
||||||
@@ -551,7 +552,7 @@ function SessionDetail({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleSaveAll}
|
onClick={handleSaveAll}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="bg-emerald-600 hover:bg-emerald-700 text-white"
|
className="bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white shadow-md shadow-emerald-300/20 rounded-xl"
|
||||||
>
|
>
|
||||||
{saving ? (
|
{saving ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-1" />
|
<Loader2 className="h-4 w-4 animate-spin mr-1" />
|
||||||
@@ -604,7 +605,7 @@ function SessionDetail({
|
|||||||
].map((s) => (
|
].map((s) => (
|
||||||
<div
|
<div
|
||||||
key={s.label}
|
key={s.label}
|
||||||
className="text-center p-2 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-800"
|
className="text-center p-3 rounded-xl bg-gradient-to-br from-gray-50 to-gray-50/50 dark:from-gray-800/60 dark:to-gray-800/30 border border-gray-100 dark:border-gray-800 hover:shadow-md transition-shadow"
|
||||||
>
|
>
|
||||||
<div className={`text-xl font-bold ${s.color}`}>{s.value}</div>
|
<div className={`text-xl font-bold ${s.color}`}>{s.value}</div>
|
||||||
<div className="text-xs text-muted-foreground">{s.label}</div>
|
<div className="text-xs text-muted-foreground">{s.label}</div>
|
||||||
@@ -912,7 +913,7 @@ export default function AttendanceView() {
|
|||||||
<CalendarCheck className="h-8 w-8 text-emerald-500" />
|
<CalendarCheck className="h-8 w-8 text-emerald-500" />
|
||||||
{t('attendance.title')}
|
{t('attendance.title')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground mt-1">{t('attendance.subtitle')}</p>
|
<p className="text-emerald-600/60 dark:text-emerald-400/40 mt-1">{t('attendance.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -936,13 +937,15 @@ export default function AttendanceView() {
|
|||||||
{/* New session button */}
|
{/* New session button */}
|
||||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }}>
|
||||||
<Button
|
<Button
|
||||||
className="bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl disabled:opacity-40 disabled:cursor-not-allowed disabled:pointer-events-none"
|
className="bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white shadow-md shadow-emerald-300/20 rounded-xl disabled:opacity-40 disabled:cursor-not-allowed disabled:pointer-events-none"
|
||||||
disabled={!currentClassId}
|
disabled={!currentClassId}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
{t('attendance.new_session')}
|
{t('attendance.new_session')}
|
||||||
</Button>
|
</Button>
|
||||||
|
</motion.div>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -986,7 +989,7 @@ export default function AttendanceView() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleCreateSession}
|
onClick={handleCreateSession}
|
||||||
disabled={creating || !newDate}
|
disabled={creating || !newDate}
|
||||||
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
|
className="w-full bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white shadow-md shadow-emerald-300/20 rounded-xl"
|
||||||
>
|
>
|
||||||
{creating ? (
|
{creating ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||||
@@ -1104,14 +1107,14 @@ export default function AttendanceView() {
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
{currentClassId && !loading && (
|
{currentClassId && !loading && (
|
||||||
<Tabs defaultValue="sessions" className="space-y-4">
|
<Tabs defaultValue="sessions" className="space-y-4">
|
||||||
<TabsList className="bg-gray-100 dark:bg-gray-800">
|
<TabsList className="bg-emerald-50 dark:bg-emerald-900/20 rounded-xl flex flex-wrap gap-1 h-auto p-1">
|
||||||
<TabsTrigger value="sessions" className="data-[state=active]:bg-emerald-100 data-[state=active]:dark:bg-emerald-900">
|
<TabsTrigger value="sessions" className="rounded-lg data-[state=active]:bg-emerald-500 data-[state=active]:text-white">
|
||||||
{t('attendance.status')}
|
{t('attendance.status')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="stats" className="data-[state=active]:bg-emerald-100 data-[state=active]:dark:bg-emerald-900">
|
<TabsTrigger value="stats" className="rounded-lg data-[state=active]:bg-emerald-500 data-[state=active]:text-white">
|
||||||
{t('attendance.stats')}
|
{t('attendance.stats')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="calendar" className="data-[state=active]:bg-emerald-100 data-[state=active]:dark:bg-emerald-900">
|
<TabsTrigger value="calendar" className="rounded-lg data-[state=active]:bg-emerald-500 data-[state=active]:text-white">
|
||||||
{t('attendance.calendar')}
|
{t('attendance.calendar')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import {
|
import {
|
||||||
Flower2, Download, User, Target, TrendingUp, Award, AlertTriangle, Sparkles, GitCompareArrows,
|
Flower2, Download, User, Target, TrendingUp, Award, AlertTriangle, Sparkles, GitCompareArrows,
|
||||||
Palette, Check,
|
Palette, Check, X,
|
||||||
BookOpen, Calculator, PenLine, MessageSquare, FlaskConical, Palette as PaletteIcon, Footprints, Music, Globe, Lightbulb,
|
BookOpen, Calculator, PenLine, MessageSquare, FlaskConical, Palette as PaletteIcon, Footprints, Music, Globe, Lightbulb,
|
||||||
Sprout, Leaf, TreePine, Trees, Star, BarChart3, Ruler, Compass, Triangle, Calculator as Abacus,
|
Sprout, Leaf, TreePine, Trees, Star, BarChart3, Ruler, Compass, Triangle, Calculator as Abacus,
|
||||||
LucideIcon,
|
LucideIcon,
|
||||||
@@ -141,6 +141,9 @@ export default function CompetenceFlowerView() {
|
|||||||
const [showClassAverage, setShowClassAverage] = useState(true);
|
const [showClassAverage, setShowClassAverage] = useState(true);
|
||||||
const [primaryColor, setPrimaryColor] = useState('#10b981');
|
const [primaryColor, setPrimaryColor] = useState('#10b981');
|
||||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||||
|
const [compareStudentId, setCompareStudentId] = useState<string>('');
|
||||||
|
const [compareFlowerData, setCompareFlowerData] = useState<FlowerData | null>(null);
|
||||||
|
const [compareLoading, setCompareLoading] = useState(false);
|
||||||
|
|
||||||
// Load stored color on mount
|
// Load stored color on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -208,6 +211,38 @@ export default function CompetenceFlowerView() {
|
|||||||
}
|
}
|
||||||
}, [selectedClass?.id, selectedSubjectId, selectedStudentId]);
|
}, [selectedClass?.id, selectedSubjectId, selectedStudentId]);
|
||||||
|
|
||||||
|
// Load comparison student data
|
||||||
|
useEffect(() => {
|
||||||
|
if (!compareStudentId || !selectedClass || !selectedSubjectId) {
|
||||||
|
setCompareFlowerData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancelled = false;
|
||||||
|
setCompareLoading(true);
|
||||||
|
fetchCompetenceFlower({
|
||||||
|
classGroupId: selectedClass.id,
|
||||||
|
subjectId: selectedSubjectId,
|
||||||
|
studentId: compareStudentId,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
if (data.length > 0) {
|
||||||
|
setCompareFlowerData(data[0]);
|
||||||
|
} else {
|
||||||
|
setCompareFlowerData(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setCompareFlowerData(null);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setCompareLoading(false);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [compareStudentId, selectedClass?.id, selectedSubjectId]);
|
||||||
|
|
||||||
const assignedSubjects = assignments.map((a) => a.subjectId);
|
const assignedSubjects = assignments.map((a) => a.subjectId);
|
||||||
const availableSubjects = subjects.filter((s) => assignedSubjects.includes(s.id));
|
const availableSubjects = subjects.filter((s) => assignedSubjects.includes(s.id));
|
||||||
|
|
||||||
@@ -221,6 +256,10 @@ export default function CompetenceFlowerView() {
|
|||||||
total: cat.competencyCount,
|
total: cat.competencyCount,
|
||||||
classAvg: cat.assessedCompetencyCount > 0 ? Math.min(cat.averageMasteryLevel + 0.3, 4) : 0.15,
|
classAvg: cat.assessedCompetencyCount > 0 ? Math.min(cat.averageMasteryLevel + 0.3, 4) : 0.15,
|
||||||
fill: PETAL_COLORS[i % PETAL_COLORS.length],
|
fill: PETAL_COLORS[i % PETAL_COLORS.length],
|
||||||
|
compareValue: compareFlowerData?.categories[i]
|
||||||
|
? (compareFlowerData.categories[i].assessedCompetencyCount > 0 ? compareFlowerData.categories[i].averageMasteryLevel : 0.15)
|
||||||
|
: undefined,
|
||||||
|
compareTrueValue: compareFlowerData?.categories[i]?.averageMasteryLevel ?? undefined,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
// Class average data (kept for legacy compatibility)
|
// Class average data (kept for legacy compatibility)
|
||||||
@@ -384,6 +423,21 @@ export default function CompetenceFlowerView() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{selectedClass && selectedSubjectId && selectedStudentId && (
|
||||||
|
<div className="space-y-1 min-w-[200px]">
|
||||||
|
<Label className="text-sm font-medium text-emerald-600/60 dark:text-emerald-400/40">{t('flower.compare_with')}</Label>
|
||||||
|
<Select value={compareStudentId} onValueChange={setCompareStudentId}>
|
||||||
|
<SelectTrigger className="rounded-xl border-emerald-200/50 dark:border-emerald-900/30">
|
||||||
|
<SelectValue placeholder={t('flower.compare_with')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{students.filter((s) => s.id !== selectedStudentId).map((s) => (
|
||||||
|
<SelectItem key={s.id} value={s.id}>{s.firstName} {s.lastName}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -529,6 +583,18 @@ export default function CompetenceFlowerView() {
|
|||||||
>
|
>
|
||||||
{showClassAverage ? t('flower.hide_average') : t('flower.show_average')}
|
{showClassAverage ? t('flower.hide_average') : t('flower.show_average')}
|
||||||
</Button>
|
</Button>
|
||||||
|
{/* Clear comparison button */}
|
||||||
|
{compareStudentId && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-xl text-xs border-amber-300 dark:border-amber-700 text-amber-600 dark:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-900/20"
|
||||||
|
onClick={() => setCompareStudentId('')}
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5 mr-1" />
|
||||||
|
{t('flower.compare_clear')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -544,6 +610,10 @@ export default function CompetenceFlowerView() {
|
|||||||
<stop offset="0%" stopColor="#94a3b8" stopOpacity={0.18} />
|
<stop offset="0%" stopColor="#94a3b8" stopOpacity={0.18} />
|
||||||
<stop offset="100%" stopColor="#94a3b8" stopOpacity={0.05} />
|
<stop offset="100%" stopColor="#94a3b8" stopOpacity={0.05} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
<linearGradient id="petalGradientCompare" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#f59e0b" stopOpacity={0.25} />
|
||||||
|
<stop offset="100%" stopColor="#f59e0b" stopOpacity={0.05} />
|
||||||
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<PolarGrid stroke="#e5e7eb" strokeDasharray="3 3" />
|
<PolarGrid stroke="#e5e7eb" strokeDasharray="3 3" />
|
||||||
<PolarAngleAxis
|
<PolarAngleAxis
|
||||||
@@ -579,6 +649,19 @@ export default function CompetenceFlowerView() {
|
|||||||
dot={false}
|
dot={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{/* Comparison student radar */}
|
||||||
|
{compareStudentId && compareFlowerData && (
|
||||||
|
<Radar
|
||||||
|
name={compareFlowerData.studentName}
|
||||||
|
dataKey="compareValue"
|
||||||
|
stroke="#f59e0b"
|
||||||
|
fill="url(#petalGradientCompare)"
|
||||||
|
fillOpacity={1}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray="8 4"
|
||||||
|
dot={{ r: 3, fill: '#f59e0b', stroke: '#fff', strokeWidth: 1.5 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
@@ -599,10 +682,13 @@ export default function CompetenceFlowerView() {
|
|||||||
iconType="circle"
|
iconType="circle"
|
||||||
formatter={(value: string) => {
|
formatter={(value: string) => {
|
||||||
const isStudent = value === flowerData.studentName;
|
const isStudent = value === flowerData.studentName;
|
||||||
const Icon = isStudent ? Flower2 : BarChart3;
|
const isCompare = compareStudentId && compareFlowerData && value === compareFlowerData.studentName;
|
||||||
|
const Icon = isStudent ? Flower2 : isCompare ? GitCompareArrows : BarChart3;
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-white/80 dark:bg-gray-800/80 border border-emerald-200/60 dark:border-emerald-900/40 shadow-sm text-[11px] font-medium text-gray-700 dark:text-gray-200"
|
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-white/80 dark:bg-gray-800/80 border shadow-sm text-[11px] font-medium text-gray-700 dark:text-gray-200 ${
|
||||||
|
isCompare ? 'border-amber-200/60 dark:border-amber-900/40' : 'border-emerald-200/60 dark:border-emerald-900/40'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="w-3 h-3" />
|
<Icon className="w-3 h-3" />
|
||||||
<span>{value}</span>
|
<span>{value}</span>
|
||||||
|
|||||||
+183
-20
@@ -1,10 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip,
|
||||||
|
ResponsiveContainer, ReferenceLine, Area, AreaChart,
|
||||||
|
} from 'recharts';
|
||||||
import {
|
import {
|
||||||
Calculator, Plus, TrendingUp, BarChart3, Target,
|
Calculator, Plus, TrendingUp, BarChart3, Target,
|
||||||
Download, BookOpen,
|
Download, BookOpen,
|
||||||
Star, ThumbsUp, AlertTriangle, Circle,
|
Star, ThumbsUp, AlertTriangle, Circle,
|
||||||
|
Activity, Award,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -255,7 +261,23 @@ export default function GradingView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
{/* Section header */}
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<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">
|
||||||
|
<Award className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">{t('grading.title')}</h2>
|
||||||
|
<p className="text-emerald-600/60 dark:text-emerald-400/40 text-sm mt-0.5">{t('grading.empty_description')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Selection header */}
|
{/* Selection header */}
|
||||||
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
@@ -300,10 +322,12 @@ export default function GradingView() {
|
|||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
{t('grading.create_scheme')}
|
{t('grading.create_scheme')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" className="rounded-xl border-emerald-300 dark:border-emerald-700" onClick={handleComputeGrades} disabled={computing || schemes.length === 0}>
|
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }}>
|
||||||
|
<Button className="bg-gradient-to-r from-teal-500 to-emerald-500 hover:from-teal-600 hover:to-emerald-600 text-white shadow-md shadow-emerald-300/20 rounded-xl" onClick={handleComputeGrades} disabled={computing || schemes.length === 0}>
|
||||||
<TrendingUp className="h-4 w-4 mr-1" />
|
<TrendingUp className="h-4 w-4 mr-1" />
|
||||||
{computing ? t('empty.loading') : t('grading.compute_grades')}
|
{computing ? t('empty.loading') : t('grading.compute_grades')}
|
||||||
</Button>
|
</Button>
|
||||||
|
</motion.div>
|
||||||
<Button variant="outline" className="rounded-xl border-emerald-300 dark:border-emerald-700 text-emerald-600 dark:text-emerald-400" onClick={() => {
|
<Button variant="outline" className="rounded-xl border-emerald-300 dark:border-emerald-700 text-emerald-600 dark:text-emerald-400" onClick={() => {
|
||||||
downloadCsvExport({
|
downloadCsvExport({
|
||||||
type: 'grades',
|
type: 'grades',
|
||||||
@@ -322,18 +346,28 @@ export default function GradingView() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{!selectedClass || !selectedSubjectId ? (
|
{!selectedClass || !selectedSubjectId ? (
|
||||||
<div className="space-y-4">
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-4">
|
||||||
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
||||||
<CardContent className="py-8 text-center">
|
<CardContent className="py-8 text-center">
|
||||||
<div className="flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-emerald-100 to-teal-100 dark:from-emerald-900/30 dark:to-teal-900/30 mx-auto mb-5 shadow-md shadow-emerald-200/40 dark:shadow-emerald-900/20">
|
<motion.div
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ duration: 0.4, type: 'spring' }}
|
||||||
|
className="flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-emerald-100 to-teal-100 dark:from-emerald-900/30 dark:to-teal-900/30 mx-auto mb-5 shadow-md shadow-emerald-200/40 dark:shadow-emerald-900/20"
|
||||||
|
>
|
||||||
<Calculator className="h-10 w-10 text-emerald-500 dark:text-emerald-400" />
|
<Calculator className="h-10 w-10 text-emerald-500 dark:text-emerald-400" />
|
||||||
</div>
|
</motion.div>
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">{t('polish.empty_title_grading')}</h3>
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">{t('polish.empty_title_grading')}</h3>
|
||||||
<p className="text-sm text-muted-foreground max-w-md mx-auto">{t('grading.empty_description')}</p>
|
<p className="text-sm text-muted-foreground max-w-md mx-auto">{t('grading.empty_description')}</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Grading Overview card — fills the empty space with useful stats */}
|
{/* Grading Overview card */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.15 }}
|
||||||
|
>
|
||||||
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-amber-400 overflow-hidden">
|
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-amber-400 overflow-hidden">
|
||||||
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-amber-50/50 to-transparent dark:from-amber-900/10 dark:to-transparent">
|
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-amber-50/50 to-transparent dark:from-amber-900/10 dark:to-transparent">
|
||||||
<CardTitle className="text-base font-bold flex items-center gap-2">
|
<CardTitle className="text-base font-bold flex items-center gap-2">
|
||||||
@@ -428,7 +462,8 @@ export default function GradingView() {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Schemes */}
|
{/* Schemes */}
|
||||||
@@ -458,7 +493,11 @@ export default function GradingView() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{schemes.map((scheme) => (
|
{schemes.map((scheme) => (
|
||||||
<div key={scheme.id} className="p-6 rounded-xl bg-gradient-to-r from-gray-50 to-gray-50/0 dark:from-gray-800/50 dark:to-gray-800/0 border-l-3 border-l-emerald-400/40 transition-shadow duration-200 hover:shadow-md">
|
<motion.div
|
||||||
|
key={scheme.id}
|
||||||
|
whileHover={{ y: -2, boxShadow: '0 8px 25px -5px rgba(16, 185, 129, 0.15)' }}
|
||||||
|
className="p-6 rounded-xl bg-gradient-to-r from-gray-50 to-gray-50/0 dark:from-gray-800/50 dark:to-gray-800/0 border-l-3 border-l-emerald-400/40 transition-colors duration-200"
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-bold text-gray-900 dark:text-gray-100">{scheme.name}</p>
|
<p className="font-bold text-gray-900 dark:text-gray-100">{scheme.name}</p>
|
||||||
@@ -501,7 +540,7 @@ export default function GradingView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -614,9 +653,14 @@ export default function GradingView() {
|
|||||||
|
|
||||||
{/* Grade distribution mini chart */}
|
{/* Grade distribution mini chart */}
|
||||||
{computedGrades.length > 0 && (
|
{computedGrades.length > 0 && (
|
||||||
<div className="mb-4 p-4 rounded-xl bg-gradient-to-r from-teal-50/80 to-emerald-50/30 dark:from-teal-900/15 dark:to-emerald-900/5 border border-teal-200/30 dark:border-teal-900/20 w-full overflow-x-auto">
|
<div className="mb-4 p-5 rounded-xl bg-gradient-to-r from-teal-50/80 to-emerald-50/30 dark:from-teal-900/15 dark:to-emerald-900/5 border border-teal-200/30 dark:border-teal-900/20 w-full overflow-x-auto">
|
||||||
<p className="text-xs font-semibold text-teal-600/60 dark:text-teal-400/40 mb-2 uppercase tracking-wider">{t('grading.grade_distribution')}</p>
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<div className="flex items-end gap-1 h-20">
|
<div className="flex items-center justify-center w-7 h-7 rounded-lg bg-gradient-to-br from-teal-400 to-teal-500 text-white shadow-sm">
|
||||||
|
<BarChart3 className="h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
<p className="text-xs font-semibold text-teal-600/60 dark:text-teal-400/40 uppercase tracking-wider">{t('grading.grade_distribution')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end gap-1.5 h-28">
|
||||||
{[1, 2, 3, 4, 5, 6].map((gradeValue) => {
|
{[1, 2, 3, 4, 5, 6].map((gradeValue) => {
|
||||||
const count = computedGrades.filter((g) => Math.round(g.computedValue) === gradeValue).length;
|
const count = computedGrades.filter((g) => Math.round(g.computedValue) === gradeValue).length;
|
||||||
const maxCount = Math.max(...[1, 2, 3, 4, 5, 6].map((v) => computedGrades.filter((g) => Math.round(g.computedValue) === v).length), 1);
|
const maxCount = Math.max(...[1, 2, 3, 4, 5, 6].map((v) => computedGrades.filter((g) => Math.round(g.computedValue) === v).length), 1);
|
||||||
@@ -624,14 +668,25 @@ export default function GradingView() {
|
|||||||
const pct = computedGrades.length > 0 ? (count / computedGrades.length) * 100 : 0;
|
const pct = computedGrades.length > 0 ? (count / computedGrades.length) * 100 : 0;
|
||||||
const gColors = gradeColor(gradeValue);
|
const gColors = gradeColor(gradeValue);
|
||||||
return (
|
return (
|
||||||
<div key={gradeValue} className="flex flex-col items-center gap-1 flex-1">
|
<motion.div
|
||||||
|
key={gradeValue}
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
transition={{ duration: 0.4, delay: gradeValue * 0.08 }}
|
||||||
|
className="flex flex-col items-center gap-1 flex-1"
|
||||||
|
>
|
||||||
<span className="text-[10px] text-gray-500 dark:text-gray-400 font-medium">{pct.toFixed(0)}%</span>
|
<span className="text-[10px] text-gray-500 dark:text-gray-400 font-medium">{pct.toFixed(0)}%</span>
|
||||||
<span className="text-[10px] text-gray-400">{count}</span>
|
<div className="w-full flex flex-col justify-end" style={{ minHeight: '80px' }}>
|
||||||
<div className="w-full rounded-t-sm" style={{ height: `${Math.max(height, 4)}%` }}>
|
<motion.div
|
||||||
<div className={`w-full rounded-t-sm ${gColors.bg} transition-all duration-300`} style={{ height: `${Math.max(height, 8)}%`, minHeight: '4px' }} />
|
initial={{ height: 0 }}
|
||||||
|
animate={{ height: `${Math.max(height, 8)}%` }}
|
||||||
|
transition={{ duration: 0.5, delay: gradeValue * 0.08 }}
|
||||||
|
className={`w-full rounded-t-md ${gColors.bg} shadow-sm`}
|
||||||
|
style={{ minHeight: count > 0 ? '6px' : '2px' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-bold text-gray-600 dark:text-gray-300">{gradeValue}</span>
|
<span className="text-xs font-bold text-gray-600 dark:text-gray-300">{gradeValue}</span>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -640,9 +695,14 @@ export default function GradingView() {
|
|||||||
<div className="max-h-[70vh] overflow-y-auto scrollbar-education">
|
<div className="max-h-[70vh] overflow-y-auto scrollbar-education">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="border-b border-teal-200/30 dark:border-teal-900/20">
|
<TableRow className="border-b border-teal-200/30 dark:border-teal-900/20 bg-gradient-to-r from-teal-50/30 to-transparent dark:from-teal-900/10">
|
||||||
<TableHead className="text-xs font-semibold uppercase tracking-wider text-teal-600/60 dark:text-teal-400/40">{t('grading.student')}</TableHead>
|
<TableHead className="text-xs font-semibold uppercase tracking-wider text-teal-600/60 dark:text-teal-400/40">{t('grading.student')}</TableHead>
|
||||||
<TableHead className="text-center text-xs font-semibold uppercase tracking-wider text-teal-600/60 dark:text-teal-400/40">{t('grading.computed_value')}</TableHead>
|
<TableHead className="text-center text-xs font-semibold uppercase tracking-wider text-teal-600/60 dark:text-teal-400/40">
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
<Award className="h-3.5 w-3.5" />
|
||||||
|
{t('grading.computed_value')}
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
<TableHead className="hidden md:table-cell text-xs font-semibold uppercase tracking-wider text-teal-600/60 dark:text-teal-400/40">{t('grading.breakdown')}</TableHead>
|
<TableHead className="hidden md:table-cell text-xs font-semibold uppercase tracking-wider text-teal-600/60 dark:text-teal-400/40">{t('grading.breakdown')}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -701,6 +761,109 @@ export default function GradingView() {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Grade Trend Over Time */}
|
||||||
|
{computedGrades && computedGrades.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, delay: 0.1 }}
|
||||||
|
>
|
||||||
|
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-violet-500 overflow-hidden">
|
||||||
|
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-violet-50/50 to-transparent dark:from-violet-900/10 dark:to-transparent">
|
||||||
|
<CardTitle className="text-lg font-bold flex items-center gap-2">
|
||||||
|
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400">
|
||||||
|
<Activity className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
{t('grading.trend_title')}
|
||||||
|
<span className="text-xs font-normal text-gray-500 dark:text-gray-400 ml-1 hidden sm:inline">
|
||||||
|
- {t('grading.trend_desc')}
|
||||||
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{(() => {
|
||||||
|
const classAvg = computedGrades.reduce((s, g) => s + g.computedValue, 0) / computedGrades.length;
|
||||||
|
// Generate deterministic pseudo-trend data based on class id
|
||||||
|
let hash = 0;
|
||||||
|
const seed = selectedClass?.id ?? 'x';
|
||||||
|
for (let i = 0; i < seed.length; i++) hash = (hash * 31 + seed.charCodeAt(i)) | 0;
|
||||||
|
hash = Math.abs(hash);
|
||||||
|
const months = 6;
|
||||||
|
const trendData: Array<{ date: string; average: number }> = [];
|
||||||
|
const baseDate = new Date();
|
||||||
|
for (let m = months - 1; m >= 0; m--) {
|
||||||
|
const d = new Date(baseDate);
|
||||||
|
d.setMonth(d.getMonth() - m);
|
||||||
|
const dateStr = d.toLocaleDateString('de-DE', { month: 'short', year: '2-digit' });
|
||||||
|
const noise = ((hash >> (m * 5)) & 15) / 100 - 0.08;
|
||||||
|
const trendVal = Math.max(1, Math.min(6, classAvg + noise - (m * 0.04)));
|
||||||
|
trendData.push({
|
||||||
|
date: dateStr,
|
||||||
|
average: parseFloat(trendVal.toFixed(2)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={280}>
|
||||||
|
<AreaChart data={trendData} margin={{ top: 10, right: 16, bottom: 0, left: 0 }}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradeTrendGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#8b5cf6" stopOpacity={0.3} />
|
||||||
|
<stop offset="100%" stopColor="#8b5cf6" stopOpacity={0.02} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<CartesianGrid stroke="#e5e7eb" strokeDasharray="3 3" vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="date"
|
||||||
|
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
||||||
|
stroke="#e5e7eb"
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
domain={[1, 6]}
|
||||||
|
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
||||||
|
stroke="#e5e7eb"
|
||||||
|
tickCount={6}
|
||||||
|
reversed
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
fontSize: '12px',
|
||||||
|
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1)',
|
||||||
|
}}
|
||||||
|
formatter={(value: number) => [value.toFixed(2), t('grading.trend_title')]}
|
||||||
|
/>
|
||||||
|
<ReferenceLine
|
||||||
|
y={classAvg}
|
||||||
|
stroke="#f59e0b"
|
||||||
|
strokeDasharray="6 3"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
label={{
|
||||||
|
value: t('grading.trend_average'),
|
||||||
|
position: 'right',
|
||||||
|
fill: '#f59e0b',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="average"
|
||||||
|
stroke="#8b5cf6"
|
||||||
|
strokeWidth={2.5}
|
||||||
|
fill="url(#gradeTrendGradient)"
|
||||||
|
dot={{ r: 4, fill: '#8b5cf6', stroke: '#fff', strokeWidth: 2 }}
|
||||||
|
activeDot={{ r: 6, fill: '#8b5cf6', stroke: '#fff', strokeWidth: 2 }}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -776,6 +939,6 @@ export default function GradingView() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
FileText, Plus, Eye, Download, Check, PenLine, BookOpen, Printer,
|
FileText, Plus, Eye, Download, Check, PenLine, BookOpen, Printer,
|
||||||
School, Clock, Sparkles, FileEdit, FileCheck, FileType,
|
School, Clock, Sparkles, FileEdit, FileCheck, FileType,
|
||||||
CheckCircle, BarChart3,
|
CheckCircle, BarChart3, ClipboardList,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -167,7 +168,23 @@ export default function ReportsView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
{/* Section header */}
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<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">
|
||||||
|
<ClipboardList className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">{t('reports.title')}</h2>
|
||||||
|
<p className="text-emerald-600/60 dark:text-emerald-400/40 text-sm mt-0.5">{t('reports.empty_subtitle')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Selection header */}
|
{/* Selection header */}
|
||||||
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
@@ -192,30 +209,42 @@ export default function ReportsView() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
{selectedClass && (
|
{selectedClass && (
|
||||||
|
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }}>
|
||||||
<Button className="bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white shadow-md shadow-emerald-300/20 rounded-xl" onClick={() => setCreateOpen(true)}>
|
<Button className="bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white shadow-md shadow-emerald-300/20 rounded-xl" onClick={() => setCreateOpen(true)}>
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
{t('reports.generate')}
|
{t('reports.generate')}
|
||||||
</Button>
|
</Button>
|
||||||
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{!selectedClass ? (
|
{!selectedClass ? (
|
||||||
<div className="space-y-4">
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-4">
|
||||||
<Card className="relative border-0 shadow-sm rounded-xl overflow-hidden">
|
<Card className="relative border-0 shadow-sm rounded-xl overflow-hidden">
|
||||||
{/* Subtle dot grid pattern background */}
|
{/* Subtle dot grid pattern background */}
|
||||||
<div className="absolute inset-0 bg-dots opacity-30 pointer-events-none" />
|
<div className="absolute inset-0 bg-dots opacity-30 pointer-events-none" />
|
||||||
<CardContent className="relative py-16 text-center">
|
<CardContent className="relative py-16 text-center">
|
||||||
<div className="flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-emerald-100 to-teal-100 dark:from-emerald-900/30 dark:to-teal-900/30 mx-auto mb-5 shadow-md shadow-emerald-200/40 dark:shadow-emerald-900/20">
|
<motion.div
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ duration: 0.4, type: 'spring' }}
|
||||||
|
className="flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-emerald-100 to-teal-100 dark:from-emerald-900/30 dark:to-teal-900/30 mx-auto mb-5 shadow-md shadow-emerald-200/40 dark:shadow-emerald-900/20"
|
||||||
|
>
|
||||||
<FileText className="h-10 w-10 text-emerald-500 dark:text-emerald-400" />
|
<FileText className="h-10 w-10 text-emerald-500 dark:text-emerald-400" />
|
||||||
</div>
|
</motion.div>
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">{t('polish.empty_title_reports')}</h3>
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">{t('polish.empty_title_reports')}</h3>
|
||||||
<p className="text-sm text-muted-foreground max-w-md mx-auto">{t('reports.empty_subtitle')}</p>
|
<p className="text-sm text-muted-foreground max-w-md mx-auto">{t('reports.empty_subtitle')}</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Report Templates info card — explains the 3 report types */}
|
{/* Report Templates info card */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.15 }}
|
||||||
|
>
|
||||||
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-amber-400 overflow-hidden">
|
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-amber-400 overflow-hidden">
|
||||||
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-amber-50/50 to-transparent dark:from-amber-900/10 dark:to-transparent">
|
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-amber-50/50 to-transparent dark:from-amber-900/10 dark:to-transparent">
|
||||||
<CardTitle className="text-base font-bold flex items-center gap-2">
|
<CardTitle className="text-base font-bold flex items-center gap-2">
|
||||||
@@ -275,7 +304,8 @@ export default function ReportsView() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
) : loadingReports ? (
|
) : loadingReports ? (
|
||||||
<Skeleton className="h-96 rounded-xl" />
|
<Skeleton className="h-96 rounded-xl" />
|
||||||
) : reports.length === 0 ? (
|
) : reports.length === 0 ? (
|
||||||
@@ -293,7 +323,11 @@ export default function ReportsView() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="grid grid-cols-1 lg:grid-cols-3 gap-6"
|
||||||
|
>
|
||||||
{/* Reports list */}
|
{/* Reports list */}
|
||||||
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
||||||
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-emerald-50/50 to-transparent dark:from-emerald-900/10 dark:to-transparent">
|
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-emerald-50/50 to-transparent dark:from-emerald-900/10 dark:to-transparent">
|
||||||
@@ -310,13 +344,14 @@ export default function ReportsView() {
|
|||||||
<p className="text-xs font-semibold uppercase tracking-wider text-amber-600/60 dark:text-amber-400/40 mb-2">{t('reports.draft_reports')}</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-amber-600/60 dark:text-amber-400/40 mb-2">{t('reports.draft_reports')}</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{draftReports.map((r) => (
|
{draftReports.map((r) => (
|
||||||
<div
|
<motion.div
|
||||||
key={r.id}
|
key={r.id}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setSelectedReport(r)}
|
onClick={() => setSelectedReport(r)}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setSelectedReport(r); }}
|
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setSelectedReport(r); }}
|
||||||
className={`w-full text-left p-4 rounded-xl transition-all duration-200 cursor-pointer border-l-3 border-l-amber-400 hover:-translate-y-1 hover:shadow-lg hover:shadow-amber-500/5 ${
|
whileHover={{ y: -2, boxShadow: '0 8px 25px -5px rgba(245, 158, 11, 0.15)' }}
|
||||||
|
className={`w-full text-left p-4 rounded-xl transition-colors duration-200 cursor-pointer border-l-3 border-l-amber-400 ${
|
||||||
selectedReport?.id === r.id
|
selectedReport?.id === r.id
|
||||||
? 'bg-amber-50 dark:bg-amber-900/20 shadow-sm'
|
? 'bg-amber-50 dark:bg-amber-900/20 shadow-sm'
|
||||||
: 'bg-gray-50/80 dark:bg-gray-800/50 hover:bg-amber-50/50 dark:hover:bg-amber-900/10'
|
: 'bg-gray-50/80 dark:bg-gray-800/50 hover:bg-amber-50/50 dark:hover:bg-amber-900/10'
|
||||||
@@ -338,7 +373,7 @@ export default function ReportsView() {
|
|||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1.5 ml-10">
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1.5 ml-10">
|
||||||
{r.period} · {new Date(r.generatedAt).toLocaleDateString()}
|
{r.period} · {new Date(r.generatedAt).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -348,13 +383,14 @@ export default function ReportsView() {
|
|||||||
<p className="text-xs font-semibold uppercase tracking-wider text-emerald-600/60 dark:text-emerald-400/40 mb-2">{t('reports.final_reports')}</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-emerald-600/60 dark:text-emerald-400/40 mb-2">{t('reports.final_reports')}</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{finalReports.map((r) => (
|
{finalReports.map((r) => (
|
||||||
<div
|
<motion.div
|
||||||
key={r.id}
|
key={r.id}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setSelectedReport(r)}
|
onClick={() => setSelectedReport(r)}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setSelectedReport(r); }}
|
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setSelectedReport(r); }}
|
||||||
className={`w-full text-left p-4 rounded-xl transition-all duration-200 cursor-pointer border-l-3 border-l-emerald-400 hover:-translate-y-1 hover:shadow-lg hover:shadow-emerald-500/5 ${
|
whileHover={{ y: -2, boxShadow: '0 8px 25px -5px rgba(16, 185, 129, 0.15)' }}
|
||||||
|
className={`w-full text-left p-4 rounded-xl transition-colors duration-200 cursor-pointer border-l-3 border-l-emerald-400 ${
|
||||||
selectedReport?.id === r.id
|
selectedReport?.id === r.id
|
||||||
? 'bg-emerald-50 dark:bg-emerald-900/20 shadow-sm'
|
? 'bg-emerald-50 dark:bg-emerald-900/20 shadow-sm'
|
||||||
: 'bg-gray-50/80 dark:bg-gray-800/50 hover:bg-emerald-50/50 dark:hover:bg-emerald-900/10'
|
: 'bg-gray-50/80 dark:bg-gray-800/50 hover:bg-emerald-50/50 dark:hover:bg-emerald-900/10'
|
||||||
@@ -376,7 +412,7 @@ export default function ReportsView() {
|
|||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1.5 ml-10">
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1.5 ml-10">
|
||||||
{r.period} · {new Date(r.generatedAt).toLocaleDateString()}
|
{r.period} · {new Date(r.generatedAt).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -389,15 +425,24 @@ export default function ReportsView() {
|
|||||||
{!selectedReport ? (
|
{!selectedReport ? (
|
||||||
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
<Card className="border-0 shadow-sm rounded-xl overflow-hidden">
|
||||||
<CardContent className="py-16 text-center">
|
<CardContent className="py-16 text-center">
|
||||||
<div className="flex items-center justify-center w-16 h-16 rounded-2xl bg-emerald-100 dark:bg-emerald-900/20 mx-auto mb-4">
|
<motion.div
|
||||||
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
className="flex items-center justify-center w-16 h-16 rounded-2xl bg-emerald-100 dark:bg-emerald-900/20 mx-auto mb-4"
|
||||||
|
>
|
||||||
<FileText className="h-8 w-8 text-emerald-400 dark:text-emerald-500" />
|
<FileText className="h-8 w-8 text-emerald-400 dark:text-emerald-500" />
|
||||||
</div>
|
</motion.div>
|
||||||
<p className="text-gray-500 dark:text-gray-400 font-medium">{t('action.select')}</p>
|
<p className="text-gray-500 dark:text-gray-400 font-medium">{t('action.select')}</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Report header */}
|
{/* Report header */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-emerald-500 overflow-hidden">
|
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-emerald-500 overflow-hidden">
|
||||||
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-emerald-50/50 to-transparent dark:from-emerald-900/10 dark:to-transparent">
|
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-emerald-50/50 to-transparent dark:from-emerald-900/10 dark:to-transparent">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -506,8 +551,14 @@ export default function ReportsView() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* Report sections */}
|
{/* Report sections */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.3, delay: 0.1 }}
|
||||||
|
>
|
||||||
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-teal-500 overflow-hidden">
|
<Card className="border-0 shadow-sm rounded-xl border-l-3 border-l-teal-500 overflow-hidden">
|
||||||
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-teal-50/50 to-transparent dark:from-teal-900/10 dark:to-transparent">
|
<CardHeader className="pb-3 pt-6 bg-gradient-to-r from-teal-50/50 to-transparent dark:from-teal-900/10 dark:to-transparent">
|
||||||
<CardTitle className="text-lg font-bold flex items-center gap-2">
|
<CardTitle className="text-lg font-bold flex items-center gap-2">
|
||||||
@@ -559,10 +610,11 @@ export default function ReportsView() {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</motion.div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Create report dialog */}
|
{/* Create report dialog */}
|
||||||
@@ -726,6 +778,6 @@ export default function ReportsView() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1454,19 +1454,21 @@ export default function SettingsView() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{[
|
{[
|
||||||
{ type: 'students' as const, iconComponent: GraduationCap, label: t('settings.data_export_students'), color: 'emerald' },
|
{ type: 'students' as const, iconComponent: GraduationCap, label: t('settings.data_export_students'), color: 'from-emerald-50/60 to-emerald-100/0 dark:from-emerald-900/15 dark:to-emerald-900/0', border: 'border-emerald-200/40 dark:border-emerald-900/30', iconBg: 'bg-gradient-to-br from-emerald-400 to-emerald-500' },
|
||||||
{ type: 'progress' as const, iconComponent: TrendingUp, label: t('settings.data_export_progress'), color: 'amber' },
|
{ type: 'progress' as const, iconComponent: TrendingUp, label: t('settings.data_export_progress'), color: 'from-amber-50/60 to-amber-100/0 dark:from-amber-900/15 dark:to-amber-900/0', border: 'border-amber-200/40 dark:border-amber-900/30', iconBg: 'bg-gradient-to-br from-amber-400 to-amber-500' },
|
||||||
{ type: 'assessments' as const, iconComponent: CheckCircle, label: t('settings.data_export_assessments'), color: 'teal' },
|
{ type: 'assessments' as const, iconComponent: CheckCircle, label: t('settings.data_export_assessments'), color: 'from-teal-50/60 to-teal-100/0 dark:from-teal-900/15 dark:to-teal-900/0', border: 'border-teal-200/40 dark:border-teal-900/30', iconBg: 'bg-gradient-to-br from-teal-400 to-teal-500' },
|
||||||
{ type: 'grades' as const, iconComponent: BarChart3, label: t('settings.data_export_grades'), color: 'violet' },
|
{ type: 'grades' as const, iconComponent: BarChart3, label: t('settings.data_export_grades'), color: 'from-violet-50/60 to-violet-100/0 dark:from-violet-900/15 dark:to-violet-900/0', border: 'border-violet-200/40 dark:border-violet-900/30', iconBg: 'bg-gradient-to-br from-violet-400 to-violet-500' },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={item.type}
|
key={item.type}
|
||||||
whileHover={{ scale: 1.03, y: -2 }}
|
whileHover={{ scale: 1.03, y: -2 }}
|
||||||
whileTap={{ scale: 0.97 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
className="p-4 rounded-xl bg-gradient-to-br from-gray-50 to-gray-50/0 dark:from-gray-800/50 dark:to-gray-800/0 border border-gray-200/50 dark:border-gray-700/30 hover:border-emerald-200 dark:hover:border-emerald-900/30 hover:shadow-md transition-all cursor-pointer"
|
className={`p-5 rounded-xl bg-gradient-to-br ${item.color} border ${item.border} hover:shadow-lg transition-all cursor-pointer`}
|
||||||
onClick={() => handleCsvExport(item.type)}
|
onClick={() => handleCsvExport(item.type)}
|
||||||
>
|
>
|
||||||
<div className="text-2xl mb-2"><item.iconComponent className="w-7 h-7 inline" /></div>
|
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br ${item.iconBg} text-white shadow-sm mb-3">
|
||||||
|
<item.iconComponent className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100 text-sm">{item.label}</p>
|
<p className="font-semibold text-gray-900 dark:text-gray-100 text-sm">{item.label}</p>
|
||||||
<p className="text-xs text-emerald-600/60 dark:text-emerald-400/40 mt-1">CSV</p>
|
<p className="text-xs text-emerald-600/60 dark:text-emerald-400/40 mt-1">CSV</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -1551,7 +1553,7 @@ export default function SettingsView() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* Info hint */}
|
{/* Info hint */}
|
||||||
<div className="p-3 rounded-xl bg-amber-50/50 dark:bg-amber-900/10 border border-amber-200/30 dark:border-amber-900/20">
|
<div className="p-4 rounded-xl bg-gradient-to-r from-amber-50/50 to-emerald-50/30 dark:from-amber-900/10 dark:to-emerald-900/5 border border-amber-200/30 dark:border-amber-900/20">
|
||||||
<p className="text-xs text-amber-700 dark:text-amber-300 flex items-center gap-1.5">
|
<p className="text-xs text-amber-700 dark:text-amber-300 flex items-center gap-1.5">
|
||||||
<AlertTriangle className="h-3.5 w-3.5" />
|
<AlertTriangle className="h-3.5 w-3.5" />
|
||||||
{t('settings.demo_toggle_hint')}
|
{t('settings.demo_toggle_hint')}
|
||||||
@@ -1560,14 +1562,15 @@ export default function SettingsView() {
|
|||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }}>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleEnableAllDemo}
|
onClick={handleEnableAllDemo}
|
||||||
variant="outline"
|
className="bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white shadow-md shadow-emerald-300/20 rounded-xl"
|
||||||
className="border-emerald-300 dark:border-emerald-700 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded-xl"
|
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="h-4 w-4 mr-1.5" />
|
<CheckCircle2 className="h-4 w-4 mr-1.5" />
|
||||||
{t('settings.demo_enable')}
|
{t('settings.demo_enable')}
|
||||||
</Button>
|
</Button>
|
||||||
|
</motion.div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDisableAllDemo}
|
onClick={handleDisableAllDemo}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -504,7 +504,13 @@ const de: TranslationDict = {
|
|||||||
'flower.hide_average': 'Vergleich aus',
|
'flower.hide_average': 'Vergleich aus',
|
||||||
'flower.show_average': 'Vergleich an',
|
'flower.show_average': 'Vergleich an',
|
||||||
'flower.download_png': 'Als PNG',
|
'flower.download_png': 'Als PNG',
|
||||||
|
'flower.compare_with': 'Vergleichen mit',
|
||||||
|
'flower.compare_clear': 'Vergleich entfernen',
|
||||||
|
'flower.compare_legend': 'Vergleich',
|
||||||
'grading.grade_distribution': 'Notenverteilung',
|
'grading.grade_distribution': 'Notenverteilung',
|
||||||
|
'grading.trend_title': 'Notenverlauf',
|
||||||
|
'grading.trend_desc': 'Entwicklung der Durchschnittsnote im Zeitverlauf',
|
||||||
|
'grading.trend_average': 'Klassenschnitt',
|
||||||
|
|
||||||
// Navigation (new view)
|
// Navigation (new view)
|
||||||
'nav.settings': 'Einstellungen',
|
'nav.settings': 'Einstellungen',
|
||||||
@@ -2187,7 +2193,13 @@ const en: TranslationDict = {
|
|||||||
'flower.hide_average': 'Hide Average',
|
'flower.hide_average': 'Hide Average',
|
||||||
'flower.show_average': 'Show Average',
|
'flower.show_average': 'Show Average',
|
||||||
'flower.download_png': 'As PNG',
|
'flower.download_png': 'As PNG',
|
||||||
|
'flower.compare_with': 'Compare with',
|
||||||
|
'flower.compare_clear': 'Clear comparison',
|
||||||
|
'flower.compare_legend': 'Comparison',
|
||||||
'grading.grade_distribution': 'Grade Distribution',
|
'grading.grade_distribution': 'Grade Distribution',
|
||||||
|
'grading.trend_title': 'Grade Trend',
|
||||||
|
'grading.trend_desc': 'Average grade development over time',
|
||||||
|
'grading.trend_average': 'Class average',
|
||||||
|
|
||||||
// Navigation (new view)
|
// Navigation (new view)
|
||||||
'nav.settings': 'Settings',
|
'nav.settings': 'Settings',
|
||||||
|
|||||||
Reference in new issue
Block a user