From 4e2eb05fbfeabf23960b932557f974f021961c05 Mon Sep 17 00:00:00 2001 From: Mustafa Cetin Date: Wed, 29 Jul 2026 17:02:28 +0000 Subject: [PATCH] Round 19c: Professional Notebooks, Enhanced Timetable/Resources/Homework, Bug Fixes Notebook Enhancements: - Page numbering (auto page numbers at bottom) - German-style margin lines on lined pages (rote Randlinie) - Auto date stamp on new pages - Sticker/stamp collection for educational rewards - Page corner fold (dog-ear) on bookmarked pages - Washi tape decorative strips Timetable View: - Weekly grid view (Mon-Fri, periods 1-6) - Color-coded subjects - Current period highlight - Room/location display - Drag-and-drop reschedule Resource Library: - Categories (worksheets, presentations, videos, links) - Search/filter bar - Resource preview cards with thumbnails - Favorite/bookmark functionality Homework View: - Calendar view for due dates - Submission status indicators - Grade display for graded homework - My Homework view for students Settings Bug Fix: - Fixed TabsContent outside Tabs component (caused runtime crash) Drawing Canvas: - Added ruler/straight edge tool 100+ new i18n keys (DE/EN) --- src/components/drawing-canvas.tsx | 6 +- src/components/homework-view.tsx | 737 +++++++++++++++++------ src/components/notebooks-view.tsx | 415 +++++++++++-- src/components/resource-library-view.tsx | 408 ++++++++++--- src/components/settings-view.tsx | 4 +- src/components/timetable-view.tsx | 651 +++++++++++++++----- src/lib/i18n.ts | 116 ++++ worklog.md | 99 +++ 8 files changed, 1988 insertions(+), 448 deletions(-) diff --git a/src/components/drawing-canvas.tsx b/src/components/drawing-canvas.tsx index 45f9755..d2401ef 100644 --- a/src/components/drawing-canvas.tsx +++ b/src/components/drawing-canvas.tsx @@ -218,9 +218,9 @@ function drawBackground( ctx.lineTo(w, y); ctx.stroke(); } - // Margin line - ctx.strokeStyle = '#D1D5DB'; - ctx.lineWidth = 1.5; + // German-style red margin line (like Schulhefte) + ctx.strokeStyle = 'rgba(239, 68, 68, 0.35)'; + ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(64, 0); ctx.lineTo(64, h); diff --git a/src/components/homework-view.tsx b/src/components/homework-view.tsx index 38e8743..1a25d67 100644 --- a/src/components/homework-view.tsx +++ b/src/components/homework-view.tsx @@ -14,6 +14,8 @@ import { Clock, ChevronDown, ChevronUp, + ChevronLeft, + ChevronRight, X, Check, AlertTriangle, @@ -27,6 +29,16 @@ import { Star, ArrowRight, MoreHorizontal, + CalendarDays, + List, + LayoutGrid, + RefreshCw, + AlertCircle, + CheckCircle2, + Circle, + XCircle, + Trophy, + User, } from 'lucide-react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -142,6 +154,36 @@ function getDueDateColor(status: 'overdue' | 'today' | 'upcoming'): string { } } +function getStatusBadge(status: string): string { + switch (status) { + case 'pending': return 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'; + case 'submitted': return 'bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300'; + case 'graded': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300'; + case 'late': return 'bg-rose-100 text-rose-700 dark:bg-rose-950 dark:text-rose-300'; + default: return 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'; + } +} + +function getStatusLabel(status: string): string { + switch (status) { + case 'pending': return t('homework.status_pending'); + case 'submitted': return t('homework.status_submitted'); + case 'graded': return t('homework.status_graded'); + case 'late': return t('homework.status_late'); + default: return status; + } +} + +function getStatusIcon(status: string) { + switch (status) { + case 'pending': return Circle; + case 'submitted': return CheckCircle2; + case 'graded': return Trophy; + case 'late': return XCircle; + default: return Circle; + } +} + function getHomeworkTypeIcon(type: string) { switch (type) { case 'assignment': return FileText; @@ -154,43 +196,226 @@ function getHomeworkTypeIcon(type: string) { } function getHomeworkTypeLabel(type: string): string { - const key = `homework.type_${type}`; - const label = t(key); - return label === key ? type : label; + switch (type) { + case 'assignment': return t('homework.type_assignment'); + case 'reading': return t('homework.type_reading'); + case 'project': return t('homework.type_project'); + case 'practice': return t('homework.type_practice'); + case 'research': return t('homework.type_research'); + default: return type; + } } -function getStatusBadge(status: string) { - const colors: Record = { - pending: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300', - submitted: 'bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300', - graded: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300', - late: 'bg-rose-100 text-rose-700 dark:bg-rose-950 dark:text-rose-300', +function getHomeworkTypeColor(type: string): string { + switch (type) { + case 'assignment': return 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300'; + case 'reading': return 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300'; + case 'project': return 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300'; + case 'practice': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300'; + case 'research': return 'bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300'; + default: return 'bg-gray-100 text-gray-700 dark:bg-gray-900/40 dark:text-gray-300'; + } +} + +// ── Calendar Helpers ──────────────────────────────────────────────── +function getCalendarDays(year: number, month: number): (number | null)[] { + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const startOffset = firstDay === 0 ? 6 : firstDay - 1; // Mon-based + const days: (number | null)[] = []; + for (let i = 0; i < startOffset; i++) days.push(null); + for (let d = 1; d <= daysInMonth; d++) days.push(d); + return days; +} + +const MONTH_NAMES_DE = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; +const MONTH_NAMES_EN = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + +function getMonthName(month: number, locale: string): string { + return locale === 'de' ? MONTH_NAMES_DE[month] : MONTH_NAMES_EN[month]; +} + +// ── Loading Skeleton ──────────────────────────────────────────────── +function HomeworkSkeleton() { + return ( +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( + + + + + + + + ))} +
+ ); +} + +function CalendarSkeleton() { + return ( + + +
+ {[...Array(35)].map((_, i) => ( + + ))} +
+
+
+ ); +} + +// ── Calendar View Component ───────────────────────────────────────── +function HomeworkCalendarView({ + homeworks, + onHomeworkClick, + locale, +}: { + homeworks: HomeworkItem[]; + onHomeworkClick: (hw: HomeworkItem) => void; + locale: string; +}) { + const now = new Date(); + const [currentYear, setCurrentYear] = useState(now.getFullYear()); + const [currentMonth, setCurrentMonth] = useState(now.getMonth()); + + const calendarDays = useMemo(() => getCalendarDays(currentYear, currentMonth), [currentYear, currentMonth]); + + // Map homework to dates + const homeworkByDate = useMemo(() => { + const map: Record = {}; + for (const hw of homeworks) { + const due = new Date(hw.dueDate); + const key = `${due.getFullYear()}-${due.getMonth()}-${due.getDate()}`; + if (!map[key]) map[key] = []; + map[key].push(hw); + } + return map; + }, [homeworks]); + + const prevMonth = () => { + if (currentMonth === 0) { setCurrentMonth(11); setCurrentYear(currentYear - 1); } + else setCurrentMonth(currentMonth - 1); }; - return colors[status] || colors.pending; + + const nextMonth = () => { + if (currentMonth === 11) { setCurrentMonth(0); setCurrentYear(currentYear + 1); } + else setCurrentMonth(currentMonth + 1); + }; + + const goToToday = () => { + setCurrentYear(now.getFullYear()); + setCurrentMonth(now.getMonth()); + }; + + const isToday = (day: number) => { + return day === now.getDate() && currentMonth === now.getMonth() && currentYear === now.getFullYear(); + }; + + return ( + + +
+ + + {getMonthName(currentMonth, locale)} {currentYear} + +
+ + + +
+
+
+ + {/* Day headers */} +
+ {[t('timetable.mon_short'), t('timetable.tue_short'), t('timetable.wed_short'), t('timetable.thu_short'), t('timetable.fri_short'), t('homework.sat_short'), t('homework.sun_short')].map((day) => ( +
+ {day} +
+ ))} +
+ {/* Calendar grid */} +
+ {calendarDays.map((day, i) => { + if (day === null) { + return
; + } + const key = `${currentYear}-${currentMonth}-${day}`; + const dayHomeworks = homeworkByDate[key] ?? []; + const today = isToday(day); + + return ( +
+
+ {day} +
+
+ {dayHomeworks.slice(0, 2).map((hw) => { + const dueStatus = getDueDateStatus(hw.dueDate); + return ( + + ); + })} + {dayHomeworks.length > 2 && ( +
+{dayHomeworks.length - 2}
+ )} +
+
+ ); + })} +
+ + + ); } -function getStatusLabel(status: string): string { - const key = `homework.status_${status}`; - const label = t(key); - return label === key ? status : label; -} - -/* ── Main Component ────────────────────────────────────────────── */ +/* ── Main Component ─────────────────────────────────────────────── */ export default function HomeworkView() { const currentUser = useAppStore((s) => s.currentUser); - const currentClassId = useAppStore((s) => s.currentClassId); const locale = useAppStore((s) => s.locale); + const currentClassId = useAppStore((s) => s.currentClassId); const [homeworks, setHomeworks] = useState([]); const [classes, setClasses] = useState([]); const [subjects, setSubjects] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); - const [filterClass, setFilterClass] = useState('all'); - const [filterSubject, setFilterSubject] = useState('all'); - const [filterType, setFilterType] = useState('all'); - const [filterStatus, setFilterStatus] = useState('all'); + const [filterClass, setFilterClass] = useState('all'); + const [filterSubject, setFilterSubject] = useState('all'); + const [filterType, setFilterType] = useState('all'); + const [filterStatus, setFilterStatus] = useState('all'); + const [viewMode, setViewMode] = useState<'grid' | 'list' | 'calendar'>('grid'); // Create dialog const [createOpen, setCreateOpen] = useState(false); @@ -247,6 +472,7 @@ export default function HomeworkView() { const loadHomework = useCallback(() => { if (!currentUser?.schoolId) return; setLoading(true); + setError(null); const params = new URLSearchParams({ schoolId: currentUser.schoolId }); if (filterClass !== 'all') params.set('classGroupId', filterClass); if (filterSubject !== 'all') params.set('subjectId', filterSubject); @@ -257,7 +483,10 @@ export default function HomeworkView() { setHomeworks(data); setLoading(false); }) - .catch(() => setLoading(false)); + .catch((err) => { + setError(err instanceof Error ? err.message : t('homework.load_error')); + setLoading(false); + }); }, [currentUser?.schoolId, filterClass, filterSubject, filterType, isStudent]); useEffect(() => { @@ -454,114 +683,147 @@ export default function HomeworkView() {

- {t('homework.title')} + {isStudent ? t('homework.my_homework') : t('homework.title')}

{t('homework.subtitle')}

- {isTeacherOrAdmin && ( - - - - - - - {t('homework.create_title')} - {t('homework.create_description')} - -
-
- - setCreateForm({ ...createForm, title: e.target.value })} - placeholder={t('homework.field_title_placeholder')} - /> -
-
- - -
-
- - -
-
- - -
-
- - setCreateForm({ ...createForm, dueDate: e.target.value })} - /> -
-
- - setCreateForm({ ...createForm, maxPoints: e.target.value })} - placeholder={t('homework.field_max_points_placeholder')} - /> -
-
- -