From afb33974a085ce0ad6c8cb879cb9fcadc1fefe3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Szyma=C5=84ski?= Date: Fri, 26 Sep 2025 09:05:54 +0200 Subject: [PATCH 1/6] Add timetable printing support --- package.json | 1 + pnpm-lock.yaml | 12 + src/app/globals.css | 54 ++++ src/components/timetable/Timetable.tsx | 356 ++++++++++++++++++------- src/components/topbar/Buttons.tsx | 44 ++- src/stores/timetable.ts | 4 + 6 files changed, 361 insertions(+), 110 deletions(-) diff --git a/package.json b/package.json index cc7c409d..bd590fa7 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "next-themes": "^0.4.6", "react": "19.1.1", "react-dom": "19.1.1", + "react-to-print": "^3.1.1", "require-in-the-middle": "^7.5.2", "rrule": "^2.8.1", "sharp": "^0.34.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49b3f96f..42d183dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: react-dom: specifier: 19.1.1 version: 19.1.1(react@19.1.1) + react-to-print: + specifier: ^3.1.1 + version: 3.1.1(react@19.1.1) require-in-the-middle: specifier: ^7.5.2 version: 7.5.2 @@ -2991,6 +2994,11 @@ packages: '@types/react': optional: true + react-to-print@3.1.1: + resolution: {integrity: sha512-N0MUMhpl8nkGri13BjP7zusj3B/j+1eMOTt8N8PYuhBYGzA4PqTXqcihJ9cZw996dvhV6mBdwafIQCg3Ap5bKg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ~19 + react@19.1.1: resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} engines: {node: '>=0.10.0'} @@ -6479,6 +6487,10 @@ snapshots: optionalDependencies: '@types/react': 19.1.13 + react-to-print@3.1.1(react@19.1.1): + dependencies: + react: 19.1.1 + react@19.1.1: {} readdirp@3.6.0: diff --git a/src/app/globals.css b/src/app/globals.css index a28f17b6..65dd880b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -108,3 +108,57 @@ background-color: rgb(var(--accent-table)); color: #fafafa; } + +.print-container { + position: fixed; + top: -10000px; + left: -10000px; + opacity: 0; + pointer-events: none; + width: 100vw; +} + +@media print { + :root, + .dark { + --background: 254 255 254 !important; + --foreground: 246 243 244 !important; + --accent: 254 255 254 !important; + --accent-secondary: 254 255 254 !important; + --accent-table: 239 9 51 !important; + --primary: 42 23 27 !important; + --lines: 208 201 203 !important; + color-scheme: light; + } + + body { + background: #ffffff !important; + color: #111827 !important; + } + + .print-container { + position: static; + opacity: 1; + pointer-events: auto; + width: auto; + } + + .print-wrapper { + display: grid; + gap: 16px; + } + + .print-header { + display: grid; + gap: 4px; + } + + .print-table { + width: 100%; + } + + .print-no-lessons { + font-size: 1rem; + color: #374151; + } +} diff --git a/src/components/timetable/Timetable.tsx b/src/components/timetable/Timetable.tsx index d33d5dfa..be2a5756 100644 --- a/src/components/timetable/Timetable.tsx +++ b/src/components/timetable/Timetable.tsx @@ -1,12 +1,15 @@ "use client"; import { SHORT_HOURS } from "@/constants/settings"; +import { TRANSLATION_DICT } from "@/constants/translations"; import { adjustShortenedLessons } from "@/lib/adjustShortenedLessons"; import { cn } from "@/lib/utils"; import { useSettingsStore, useSettingsWithoutStore } from "@/stores/settings"; +import { useTimetableStore } from "@/stores/timetable"; import type { OptivumTimetable } from "@/types/optivum"; import { CalendarX2 } from "lucide-react"; -import { FC, useMemo, useRef } from "react"; +import { FC, useEffect, useMemo, useRef, useState } from "react"; +import { useReactToPrint } from "react-to-print"; import { ShortLessonSwitcherCell, TableHeaderCell, @@ -38,6 +41,9 @@ export const Timetable: FC = ({ timetable }) => { const setSelectedDayIndex = useSettingsWithoutStore( (state) => state.setSelectedDayIndex, ); + const setPrintTimetable = useTimetableStore( + (state) => state.setPrintTimetable, + ); const hours = useMemo(() => { if (lessonType === "custom") { @@ -94,112 +100,268 @@ export const Timetable: FC = ({ timetable }) => { touchStartX.current = null; }; - return ( -
-
- {dayNames.map((dayName) => ( - - ))} -
+ const printableRef = useRef(null); + + const formatDate = (date: Date) => + new Intl.DateTimeFormat("pl-PL", { + dateStyle: "long", + timeStyle: "short", + }).format(date); + + const [printTimestamp, setPrintTimestamp] = useState(() => formatDate(new Date())); + + const pageStyle = useMemo( + () => + `@page { size: landscape; margin: 12mm; } + html, body { + width: 100%; + background: #ffffff !important; + color: #111827 !important; + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + } + table { + border-collapse: collapse; + width: 100%; + } + th, td { + border: 1px solid #d1d5db; + padding: 8px; + vertical-align: top; + } + th { + background-color: #f3f4f6; + font-weight: 600; + } + h1, h2, p { + margin: 0; + color: #111827 !important; + } + .print-lesson { + display: grid; + gap: 2px; + margin-bottom: 6px; + } + .print-lesson:last-child { + margin-bottom: 0; + } + .print-lesson__subject { + font-weight: 600; + font-size: 0.95rem; + } + .print-lesson__meta { + color: #4b5563; + font-size: 0.85rem; + } + `, + [], + ); + + const handlePrint = useReactToPrint({ + content: () => printableRef.current, + pageStyle, + documentTitle: timetable.title + ? `Plan lekcji ${TRANSLATION_DICT[timetable.type]} ${timetable.title}` + : "Plan lekcji", + removeAfterPrint: false, + onBeforeGetContent: () => + new Promise((resolve) => { + setPrintTimestamp(formatDate(new Date())); + setTimeout(resolve, 0); + }), + }); + useEffect(() => { + setPrintTimetable(handlePrint); + return () => setPrintTimetable(null); + }, [handlePrint, setPrintTimetable]); + + return ( + <>
+
+ {dayNames.map((dayName) => ( + + ))} +
+
- {dayNames.map((_, dayIndex) => { - const dayLessons = lessons[dayIndex] ?? []; - const dayHasLessons = dayLessons.some( - (hourLessons) => hourLessons.length > 0, - ); - return ( -
- {dayHasLessons ? ( - - - {visibleHours.map((hour, hourIndex) => ( - - - - - ))} - -
- {(lessons[dayIndex]?.[hourIndex] ?? []).map( - (lessonItem, index) => ( - - ), - )} -
- ) : ( - - )} -
- ); - })} +
+ {dayNames.map((_, dayIndex) => { + const dayLessons = lessons[dayIndex] ?? []; + const dayHasLessons = dayLessons.some( + (hourLessons) => hourLessons.length > 0, + ); + return ( +
+ {dayHasLessons ? ( + + + {visibleHours.map((hour, hourIndex) => ( + + + + + ))} + +
+ {(lessons[dayIndex]?.[hourIndex] ?? []).map( + (lessonItem, index) => ( + + ), + )} +
+ ) : ( + + )} +
+ ); + })} +
-
-
- {hasLessons ? ( - - - - - {dayNames.map((dayName) => ( - +
+ {hasLessons ? ( +
- -
+ + + + {dayNames.map((dayName) => ( + + ))} + + + + {visibleHours.map((hour, hourIndex) => ( + + + {lessons.map((day, dayIndex) => ( + + ))} + ))} - - - - {visibleHours.map((hour, hourIndex) => ( - - - {lessons.map((day, dayIndex) => ( - + +
+ +
+ ) : ( + + )} +
+
+ + - + ); }; diff --git a/src/components/topbar/Buttons.tsx b/src/components/topbar/Buttons.tsx index 44195395..2b918016 100644 --- a/src/components/topbar/Buttons.tsx +++ b/src/components/topbar/Buttons.tsx @@ -1,7 +1,11 @@ +"use client"; + import { Button } from "@/components/ui/Button"; import { Skeleton } from "@/components/ui/Skeleton"; +import { useTimetableStore } from "@/stores/timetable"; import { useSettingsWithoutStore } from "@/stores/settings"; -import { MenuIcon, MoonIcon, SunMediumIcon } from "lucide-react"; +import { MenuIcon, MoonIcon, PrinterIcon, SunMediumIcon } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; import { useTheme } from "next-themes"; import { FC, useCallback } from "react"; import { useIsClient } from "usehooks-ts"; @@ -12,6 +16,8 @@ export const TopbarButtons: FC = () => { const { theme, setTheme, systemTheme } = useTheme(); const currentTheme = theme === "system" ? systemTheme : theme; + const printTimetable = useTimetableStore((state) => state.printTimetable); + const toggleSettingsPanel = useSettingsWithoutStore( (state) => state.toggleSettingsPanel, ); @@ -20,11 +26,23 @@ export const TopbarButtons: FC = () => { setTheme(currentTheme === "light" ? "dark" : "light"); }, [currentTheme, setTheme]); - const buttons = [ + interface ButtonConfig { + icon: LucideIcon; + action?: () => void; + ariaLabel: string; + disabled?: boolean; + } + + const buttons: ButtonConfig[] = [ + { + icon: PrinterIcon, + action: printTimetable ? () => printTimetable() : undefined, + ariaLabel: "Drukuj plan lekcji", + disabled: !printTimetable, + }, { icon: currentTheme === "dark" ? SunMediumIcon : MoonIcon, - href: null, - action: toggleTheme, + action: () => toggleTheme(), ariaLabel: currentTheme === "dark" ? "Przełącz na tryb jasny" @@ -32,8 +50,7 @@ export const TopbarButtons: FC = () => { }, { icon: MenuIcon, - href: null, - action: toggleSettingsPanel, + action: () => toggleSettingsPanel(), ariaLabel: "Otwórz panel ustawień", }, ]; @@ -53,13 +70,14 @@ export const TopbarButtons: FC = () => { const IconComponent = button.icon; return ( - ); diff --git a/src/stores/timetable.ts b/src/stores/timetable.ts index 98719cf8..8f0a4843 100644 --- a/src/stores/timetable.ts +++ b/src/stores/timetable.ts @@ -3,10 +3,14 @@ import { create } from "zustand"; interface TimetableStore { timetable: OptivumTimetable | null; + printTimetable: (() => void) | null; setTimetable: (timetable: OptivumTimetable) => void; + setPrintTimetable: (printFn: (() => void) | null) => void; } export const useTimetableStore = create((set) => ({ timetable: null, + printTimetable: null, setTimetable: (timetable) => set({ timetable }), + setPrintTimetable: (printFn) => set({ printTimetable: printFn }), })); From ae532be29dc93ad5a529f6a4cddf9d8050c7327b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Szyma=C5=84ski?= Date: Fri, 26 Sep 2025 09:20:14 +0200 Subject: [PATCH 2/6] Fix timetable print hook configuration --- src/components/timetable/Timetable.tsx | 90 ++++++++++++++------------ 1 file changed, 49 insertions(+), 41 deletions(-) diff --git a/src/components/timetable/Timetable.tsx b/src/components/timetable/Timetable.tsx index be2a5756..3c265e82 100644 --- a/src/components/timetable/Timetable.tsx +++ b/src/components/timetable/Timetable.tsx @@ -8,7 +8,7 @@ import { useSettingsStore, useSettingsWithoutStore } from "@/stores/settings"; import { useTimetableStore } from "@/stores/timetable"; import type { OptivumTimetable } from "@/types/optivum"; import { CalendarX2 } from "lucide-react"; -import { FC, useEffect, useMemo, useRef, useState } from "react"; +import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useReactToPrint } from "react-to-print"; import { ShortLessonSwitcherCell, @@ -102,11 +102,14 @@ export const Timetable: FC = ({ timetable }) => { const printableRef = useRef(null); - const formatDate = (date: Date) => - new Intl.DateTimeFormat("pl-PL", { - dateStyle: "long", - timeStyle: "short", - }).format(date); + const formatDate = useCallback( + (date: Date) => + new Intl.DateTimeFormat("pl-PL", { + dateStyle: "long", + timeStyle: "short", + }).format(date), + [], + ); const [printTimestamp, setPrintTimestamp] = useState(() => formatDate(new Date())); @@ -156,20 +159,25 @@ export const Timetable: FC = ({ timetable }) => { [], ); - const handlePrint = useReactToPrint({ - content: () => printableRef.current, + const triggerPrint = useReactToPrint({ + contentRef: printableRef, pageStyle, documentTitle: timetable.title ? `Plan lekcji ${TRANSLATION_DICT[timetable.type]} ${timetable.title}` : "Plan lekcji", - removeAfterPrint: false, - onBeforeGetContent: () => - new Promise((resolve) => { - setPrintTimestamp(formatDate(new Date())); + preserveAfterPrint: true, + onBeforePrint: async () => { + setPrintTimestamp(formatDate(new Date())); + await new Promise((resolve) => { setTimeout(resolve, 0); - }), + }); + }, }); + const handlePrint = useCallback(() => { + triggerPrint(); + }, [triggerPrint]); + useEffect(() => { setPrintTimetable(handlePrint); return () => setPrintTimetable(null); @@ -320,37 +328,37 @@ export const Timetable: FC = ({ timetable }) => { - {lessons.map((day, dayIndex) => ( + {lessons.map((day, dayIndex) => { + const hourLessons = hourIndex < day.length ? day[hourIndex] : []; + + return ( - {(hourIndex < day.length ? day[hourIndex] : []).map((lesson, lessonIndex) => { - const lessonDetails = [ - lesson.teacher, - lesson.className, - ] - .filter(Boolean) - .join(" • "); + {hourLessons.map((lesson, lessonIndex) => { + const lessonDetails = [ + lesson.teacher, + lesson.className, + ] + .filter(Boolean) + .join(" • "); - return ( -
- - {lesson.subject} - {lesson.groupName ? ` (${lesson.groupName})` : ""} - - {lessonDetails && ( - - {lessonDetails} + return ( +
+ + {lesson.subject} + {lesson.groupName ? ` (${lesson.groupName})` : ""} - )} - {lesson.room && ( - - Sala {lesson.room} - - )} -
- ); - })} - - ))} + {lessonDetails && ( + {lessonDetails} + )} + {lesson.room && ( + Sala {lesson.room} + )} +
+ ); + })} + + ); + })} ))} From 11cdd834e2162e257d6f9c858d4eb020109077a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Szyma=C5=84ski?= Date: Fri, 26 Sep 2025 09:31:59 +0200 Subject: [PATCH 3/6] Tighten print layout to fit on one page --- src/app/globals.css | 49 ++++++++++++++++++++++++-- src/components/timetable/Timetable.tsx | 44 ++++++++++++++++------- 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 65dd880b..5fa05f6f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -134,6 +134,9 @@ body { background: #ffffff !important; color: #111827 !important; + font-size: 11px; + line-height: 1.35; + margin: 0; } .print-container { @@ -145,20 +148,60 @@ .print-wrapper { display: grid; - gap: 16px; + gap: 12px; } .print-header { display: grid; - gap: 4px; + gap: 2px; + } + + .print-header h1 { + font-size: 1.25rem; } .print-table { width: 100%; + border-collapse: collapse; + table-layout: fixed; + } + + .print-table th, + .print-table td { + border: 1px solid #d1d5db; + padding: 4px 6px; + vertical-align: top; + word-break: break-word; + } + + .print-table th { + background-color: #f3f4f6; + font-weight: 600; + } + + .print-lesson { + display: grid; + gap: 1px; + margin-bottom: 4px; + } + + .print-lesson:last-child { + margin-bottom: 0; + } + + .print-lesson__subject { + font-weight: 600; + font-size: 0.85rem; + } + + .print-lesson__meta { + color: #4b5563; + font-size: 0.7rem; + line-height: 1.3; } .print-no-lessons { - font-size: 1rem; + font-size: 0.95rem; color: #374151; } } diff --git a/src/components/timetable/Timetable.tsx b/src/components/timetable/Timetable.tsx index 3c265e82..8cd77ded 100644 --- a/src/components/timetable/Timetable.tsx +++ b/src/components/timetable/Timetable.tsx @@ -115,45 +115,63 @@ export const Timetable: FC = ({ timetable }) => { const pageStyle = useMemo( () => - `@page { size: landscape; margin: 12mm; } + `@page { size: A4 landscape; margin: 10mm; } html, body { width: 100%; + margin: 0 !important; background: #ffffff !important; color: #111827 !important; font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 11px; + line-height: 1.35; } - table { + h1, h2, p { + margin: 0; + color: #111827 !important; + } + .print-wrapper { + display: grid; + gap: 12px; + } + .print-header h1 { + font-size: 1.25rem; + } + .print-table { border-collapse: collapse; width: 100%; + table-layout: fixed; } - th, td { + .print-table th, + .print-table td { border: 1px solid #d1d5db; - padding: 8px; + padding: 4px 6px; vertical-align: top; + word-break: break-word; } - th { + .print-table th { background-color: #f3f4f6; font-weight: 600; } - h1, h2, p { - margin: 0; - color: #111827 !important; - } .print-lesson { display: grid; - gap: 2px; - margin-bottom: 6px; + gap: 1px; + margin-bottom: 4px; } .print-lesson:last-child { margin-bottom: 0; } .print-lesson__subject { font-weight: 600; - font-size: 0.95rem; + font-size: 0.85rem; } .print-lesson__meta { color: #4b5563; - font-size: 0.85rem; + font-size: 0.7rem; + line-height: 1.3; + } + .print-no-lessons { + font-size: 0.95rem; + color: #374151; } `, [], From f7d7e2953284e9be0afe0afc6d0db8d7933c10a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Szyma=C5=84ski?= Date: Fri, 26 Sep 2025 09:49:37 +0200 Subject: [PATCH 4/6] Refine timetable print layout --- src/app/globals.css | 34 ++++- src/components/timetable/Timetable.tsx | 179 +++++++++---------------- 2 files changed, 93 insertions(+), 120 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 5fa05f6f..f1faabf7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -136,6 +136,7 @@ color: #111827 !important; font-size: 11px; line-height: 1.35; + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; } @@ -153,17 +154,22 @@ .print-header { display: grid; - gap: 2px; + gap: 4px; + padding-bottom: 6px; + border-bottom: 1px solid #e5e7eb; } .print-header h1 { font-size: 1.25rem; + color: #0f172a; } .print-table { width: 100%; border-collapse: collapse; table-layout: fixed; + border-radius: 8px; + overflow: hidden; } .print-table th, @@ -174,14 +180,29 @@ word-break: break-word; } - .print-table th { - background-color: #f3f4f6; + .print-table thead th { + background-color: #e2e8f0; font-weight: 600; + color: #0f172a; + } + + .print-table tbody tr:nth-child(odd) td { + background-color: #f8fafc; + } + + .print-table tbody tr:nth-child(even) td { + background-color: #ffffff; + } + + .print-table tbody td:first-child { + background-color: #e0e7ff; + font-weight: 600; + color: #1e3a8a; } .print-lesson { display: grid; - gap: 1px; + gap: 2px; margin-bottom: 4px; } @@ -192,16 +213,17 @@ .print-lesson__subject { font-weight: 600; font-size: 0.85rem; + color: #1f2937; } .print-lesson__meta { - color: #4b5563; + color: #475569; font-size: 0.7rem; line-height: 1.3; } .print-no-lessons { font-size: 0.95rem; - color: #374151; + color: #1f2937; } } diff --git a/src/components/timetable/Timetable.tsx b/src/components/timetable/Timetable.tsx index 8cd77ded..9bfa9e56 100644 --- a/src/components/timetable/Timetable.tsx +++ b/src/components/timetable/Timetable.tsx @@ -64,12 +64,6 @@ export const Timetable: FC = ({ timetable }) => { return Math.max(hourCount, ...lessonCounts, 0); }, [lessons, timetable.hours]); - const hasLessons = useMemo( - () => - lessons.some((day) => day.some((hourLessons) => hourLessons.length > 0)), - [lessons], - ); - const todayIndex = useMemo(() => (new Date().getDay() + 6) % 7, []); const dayNames = timetable.dayNames; const hoursList = useMemo(() => Object.values(hours), [hours]); @@ -78,6 +72,34 @@ export const Timetable: FC = ({ timetable }) => { [hoursList, maxLessons], ); + const lessonsByDay = useMemo( + () => + dayNames.map((_, dayIndex) => { + const dayLessons = lessons[dayIndex] ?? []; + return visibleHours.map((_, hourIndex) => dayLessons[hourIndex] ?? []); + }), + [dayNames, lessons, visibleHours], + ); + + const dayHasLessons = useMemo( + () => lessonsByDay.map((day) => day.some((hour) => hour.length > 0)), + [lessonsByDay], + ); + + const hasLessons = useMemo( + () => dayHasLessons.some(Boolean), + [dayHasLessons], + ); + + const printableRows = useMemo( + () => + visibleHours.map((hour, hourIndex) => ({ + hour, + lessons: lessonsByDay.map((dayLessons) => dayLessons[hourIndex] ?? []), + })), + [lessonsByDay, visibleHours], + ); + const handleDayChange = (newIndex: number) => { if (selectedDayIndex !== newIndex) { setSelectedDayIndex(newIndex); @@ -114,66 +136,7 @@ export const Timetable: FC = ({ timetable }) => { const [printTimestamp, setPrintTimestamp] = useState(() => formatDate(new Date())); const pageStyle = useMemo( - () => - `@page { size: A4 landscape; margin: 10mm; } - html, body { - width: 100%; - margin: 0 !important; - background: #ffffff !important; - color: #111827 !important; - font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - font-size: 11px; - line-height: 1.35; - } - h1, h2, p { - margin: 0; - color: #111827 !important; - } - .print-wrapper { - display: grid; - gap: 12px; - } - .print-header h1 { - font-size: 1.25rem; - } - .print-table { - border-collapse: collapse; - width: 100%; - table-layout: fixed; - } - .print-table th, - .print-table td { - border: 1px solid #d1d5db; - padding: 4px 6px; - vertical-align: top; - word-break: break-word; - } - .print-table th { - background-color: #f3f4f6; - font-weight: 600; - } - .print-lesson { - display: grid; - gap: 1px; - margin-bottom: 4px; - } - .print-lesson:last-child { - margin-bottom: 0; - } - .print-lesson__subject { - font-weight: 600; - font-size: 0.85rem; - } - .print-lesson__meta { - color: #4b5563; - font-size: 0.7rem; - line-height: 1.3; - } - .print-no-lessons { - font-size: 0.95rem; - color: #374151; - } - `, + () => "@page { size: A4 landscape; margin: 10mm; }", [], ); @@ -229,45 +192,37 @@ export const Timetable: FC = ({ timetable }) => { className="flex h-full w-full transition-transform duration-300" style={{ transform: `translateX(-${selectedDayIndex * 100}%)` }} > - {dayNames.map((_, dayIndex) => { - const dayLessons = lessons[dayIndex] ?? []; - const dayHasLessons = dayLessons.some( - (hourLessons) => hourLessons.length > 0, - ); - return ( -
- {dayHasLessons ? ( - - - {visibleHours.map((hour, hourIndex) => ( - - - - - ))} - -
- {(lessons[dayIndex]?.[hourIndex] ?? []).map( - (lessonItem, index) => ( - - ), - )} -
- ) : ( - - )} -
- ); - })} + {dayNames.map((_, dayIndex) => ( +
+ {dayHasLessons[dayIndex] ? ( + + + {printableRows.map(({ hour, lessons: hourLessons }, hourIndex) => ( + + + + + ))} + +
+ {hourLessons[dayIndex].map((lessonItem, index) => ( + + ))} +
+ ) : ( + + )} +
+ ))} @@ -285,13 +240,13 @@ export const Timetable: FC = ({ timetable }) => { - {visibleHours.map((hour, hourIndex) => ( + {printableRows.map(({ hour }, hourIndex) => ( - {lessons.map((day, dayIndex) => ( + {lessonsByDay.map((day, dayIndex) => ( = ({ timetable }) => { - {visibleHours.map((hour, hourIndex) => ( + {printableRows.map(({ hour, lessons: rowLessons }, hourIndex) => (
@@ -346,10 +301,7 @@ export const Timetable: FC = ({ timetable }) => {
- {lessons.map((day, dayIndex) => { - const hourLessons = hourIndex < day.length ? day[hourIndex] : []; - - return ( + {rowLessons.map((hourLessons, dayIndex) => ( {hourLessons.map((lesson, lessonIndex) => { const lessonDetails = [ @@ -375,8 +327,7 @@ export const Timetable: FC = ({ timetable }) => { ); })} - ); - })} + ))} ))} From 7d9d9451cd2f0e387b3032790aa48e273acea110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Szyma=C5=84ski?= Date: Fri, 26 Sep 2025 10:03:44 +0200 Subject: [PATCH 5/6] Tighten print spacing and merge lesson metadata --- src/app/globals.css | 6 +++--- src/components/timetable/Timetable.tsx | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index f1faabf7..fcc70c7c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -175,7 +175,7 @@ .print-table th, .print-table td { border: 1px solid #d1d5db; - padding: 4px 6px; + padding: 2px 4px; vertical-align: top; word-break: break-word; } @@ -202,8 +202,8 @@ .print-lesson { display: grid; - gap: 2px; - margin-bottom: 4px; + gap: 1px; + margin-bottom: 3px; } .print-lesson:last-child { diff --git a/src/components/timetable/Timetable.tsx b/src/components/timetable/Timetable.tsx index 9bfa9e56..70f97ce1 100644 --- a/src/components/timetable/Timetable.tsx +++ b/src/components/timetable/Timetable.tsx @@ -306,6 +306,7 @@ export const Timetable: FC = ({ timetable }) => { {hourLessons.map((lesson, lessonIndex) => { const lessonDetails = [ lesson.teacher, + lesson.room ? `Sala ${lesson.room}` : undefined, lesson.className, ] .filter(Boolean) @@ -320,9 +321,6 @@ export const Timetable: FC = ({ timetable }) => { {lessonDetails && ( {lessonDetails} )} - {lesson.room && ( - Sala {lesson.room} - )} ); })} From a3e81e9d21f3005f963ba1a0421b238bb95366c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Szyma=C5=84ski?= Date: Fri, 26 Sep 2025 10:15:36 +0200 Subject: [PATCH 6/6] Enable keyboard shortcut for timetable printing --- src/components/timetable/Timetable.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/components/timetable/Timetable.tsx b/src/components/timetable/Timetable.tsx index 70f97ce1..9d84b247 100644 --- a/src/components/timetable/Timetable.tsx +++ b/src/components/timetable/Timetable.tsx @@ -164,6 +164,25 @@ export const Timetable: FC = ({ timetable }) => { return () => setPrintTimetable(null); }, [handlePrint, setPrintTimetable]); + useEffect(() => { + const handleKeydown = (event: KeyboardEvent) => { + if (event.repeat) return; + + const isPrintShortcut = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "p"; + + if (!isPrintShortcut) return; + + event.preventDefault(); + handlePrint(); + }; + + window.addEventListener("keydown", handleKeydown); + + return () => { + window.removeEventListener("keydown", handleKeydown); + }; + }, [handlePrint]); + return ( <>