From cd59c02995f2f17abc2ff0aa3e72cb1bac6f6b04 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Mon, 7 Sep 2026 09:11:31 +0200 Subject: [PATCH] refactor: Geraeteeinstellungen und Formatierungs-Helfer auslagern Dritter Schnitt an WeeklyView.tsx (D1): - src/lib/weekly/device-settings.ts: Cookie-Helfer plus die vier Schluessellisten, die bewusst nur im Cookie und nicht in der Datenbank gehalten werden - src/lib/weekly/format.ts: stripHtml, invertColor, isAllDayEvent, getSomedaySlotCount 11.655 -> 11.556 Zeilen. Mit npm run build verifiziert (Exit 0). Damit sind die reinen Funktionen ohne Komponenten-State heraus, insgesamt 11.906 -> 11.556 (rund 3 %). Der Rest ist die Komponente selbst: JSX und Handler auf gemeinsamem State. Jeder weitere Schnitt braucht echtes Props-Design statt Verschieben. v1.117.7 Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 13 ++++ TODO.md | 17 +++-- package-lock.json | 4 +- package.json | 2 +- src/components/WeeklyView.tsx | 114 +++--------------------------- src/lib/weekly/device-settings.ts | 52 ++++++++++++++ src/lib/weekly/format.ts | 71 +++++++++++++++++++ 7 files changed, 162 insertions(+), 111 deletions(-) create mode 100644 src/lib/weekly/device-settings.ts create mode 100644 src/lib/weekly/format.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad80eb..92c901c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project are documented in this file. +## [1.117.7] - 2026-09-07 + +### Changed +- Geräteabhängige Einstellungen nach `src/lib/weekly/device-settings.ts` (Cookie-Helfer und + die vier Schlüssellisten, die bewusst nur im Cookie und nicht in der Datenbank landen). +- Darstellungs-Helfer nach `src/lib/weekly/format.ts` (`stripHtml`, `invertColor`, + `isAllDayEvent`, `getSomedaySlotCount`). +- `WeeklyView.tsx`: 11.655 → 11.556 Zeilen. Mit `npm run build` verifiziert (Exit 0). + +Damit sind die reinen Funktionen ohne Komponenten-State heraus — insgesamt 11.906 → 11.556 +Zeilen (rund 3 %). Der Rest ist die Komponente selbst und braucht echtes Props-Design statt +Verschieben. + ## [1.117.6] - 2026-09-07 ### Changed diff --git a/TODO.md b/TODO.md index 4576b04..5579f9b 100644 --- a/TODO.md +++ b/TODO.md @@ -128,7 +128,7 @@ Betroffen sind genau zwei Dateien: | Datei | Zeilen | |---|---:| -| `src/components/WeeklyView.tsx` | 11.655 *(war 11.906)* | +| `src/components/WeeklyView.tsx` | 11.556 *(war 11.906)* | | `src/components/SettingsSidebar.tsx` | 5.445 | **Voraussetzung:** C1 muss laufen — ohne Typecheck ist ein Refactoring dieser Größe @@ -138,8 +138,8 @@ Blindflug. Ebenso hilfreich: A2, damit es überhaupt Tests gibt. - [x] Typen nach `src/lib/weekly/types.ts` (7 Typen, 97 Zeilen) — Build verifiziert - [ ] `src/components/weekly/` anlegen - [x] Datums-/Zeitraster-Helfer nach `src/lib/weekly/date-utils.ts` (13 Funktionen, 202 Zeilen) -- [ ] Cookie-/Geräteeinstellungen nach `src/lib/weekly/device-settings.ts` -- [ ] Formatierungs-Helfer (`stripHtml`, `invertColor`, `isAllDayEvent`) auslagern +- [x] Cookie-/Geräteeinstellungen nach `src/lib/weekly/device-settings.ts` +- [x] Formatierungs-Helfer nach `src/lib/weekly/format.ts` - [ ] `getWeekNumber` existiert **dreimal** im Projekt (WeeklyView, `WeekPrintModal.tsx`, `api/user/export-week-view/route.ts`) — Duplikate gegen `date-utils.ts` ersetzen, vorher die drei Implementierungen auf Gleichheit prüfen @@ -157,7 +157,16 @@ Blindflug. Ebenso hilfreich: A2, damit es überhaupt Tests gibt. - [ ] Gemeinsame Bausteine (Farbwähler, Schalterzeile) in `settings/controls/` **Vorgehen:** In kleinen Schritten, jeder Schritt ein eigener Commit, nach jedem Schritt -Typecheck. Kein "großer Wurf" — die Datei ist zu zentral. +`npm run build`. Kein "großer Wurf" — die Datei ist zu zentral. + +**Zwischenstand nach drei Schnitten:** 11.906 → 11.556 Zeilen, also **rund 3 %**. Damit sind +die reinen Funktionen ohne Komponenten-State weitgehend heraus — das war der leichte Teil. + +Die verbleibenden ~11.500 Zeilen sind die Komponente selbst: JSX und Event-Handler, die auf +gemeinsamen State zugreifen. Jeder weitere Schnitt braucht echtes Props-Design statt +Verschieben. Realistisch pro Schnitt ein eigener Durchgang, und **erst mit Tests (A2) oder +zumindest laufendem Typecheck sinnvoll** — ein Fehler beim Herauslösen von Drag & Drop fällt +sonst wieder erst im Browser auf. **Warum:** `WeeklyView.tsx` ist ein Viertel des gesamten Quellcodes. Der Weekday-Bug von Anfang September saß genau dort und war nur deshalb schwer zu finden. diff --git a/package-lock.json b/package-lock.json index 862a718..516a187 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "my-weekly-todo-list", - "version": "1.117.6", + "version": "1.117.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "my-weekly-todo-list", - "version": "1.117.6", + "version": "1.117.7", "license": "MIT", "dependencies": { "@auth/prisma-adapter": "^2.11.1", diff --git a/package.json b/package.json index b06985e..0714048 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.117.6", + "version": "1.117.7", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index a82850b..56225e7 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -98,7 +98,16 @@ import { SlidersHorizontal, } from "lucide-react"; -const stripHtml = (html: string) => html.replace(/<[^>]*>/g, '').trim(); +import { + DEVICE_SETTINGS_KEYS, DEVICE_VIEW_SETTINGS_KEYS, DEVICE_ALSO_COOKIE_KEYS, + VIEW_SETTINGS_PROFILE_KEYS, getCookie, setCookie, resolveViewDays, + readDeviceViewCookie, writeDeviceViewCookie, +} from "@/lib/weekly/device-settings"; +import type { DeviceViewSettingKey } from "@/lib/weekly/device-settings"; +import { + stripHtml, DEFAULT_SOMEDAY_SLOT_COUNT, getSomedaySlotCount, + isAllDayEvent, invertColor, +} from "@/lib/weekly/format"; // Types import UserMenu from "./UserMenu"; @@ -124,50 +133,6 @@ const SettingsSidebar = dynamic(() => import("./SettingsSidebar"), { ssr: false const RichTextEditor = dynamic(() => import("./RichTextEditor"), { ssr: false }); const PriorityView = dynamic(() => import("./PriorityView"), { ssr: false }); -// Cookie helpers for per-device settings -// Keys here bypass the DB and save/load from cookie only — each device keeps its own value -const DEVICE_SETTINGS_KEYS = ["viewDays", "cellDuration", "startHour", "endHour", "fontSize", "showSubHourSlots"]; - -// Per-view toggle keys that are also device-specific (sidebar eye toggles) -const DEVICE_VIEW_SETTINGS_KEYS = ["showSomeday", "showAllDayEvents", "showTaskCheckboxes", "showProjectIcons", "showPriorityIcons", "weatherEnabled"] as const; -type DeviceViewSettingKey = typeof DEVICE_VIEW_SETTINGS_KEYS[number]; - -// Settings that save to DB (cross-device default) AND to cookie (device override wins on load) -const DEVICE_ALSO_COOKIE_KEYS = ["viewStyle", "showTimeGrid", "startDayOffset"]; -const VIEW_SETTINGS_PROFILE_KEYS = ["menuPosition", "showHeaderControls"]; - -function getCookie(name: string): string | null { - if (typeof document === "undefined") return null; - const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); - return match ? decodeURIComponent(match[1]) : null; -} - -function setCookie(name: string, value: string, days: number = 365) { - if (typeof document === "undefined") return; - const expires = new Date(Date.now() + days * 864e5).toUTCString(); - document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`; -} - -// Explicit browser choices take precedence over responsive defaults on every load. -function resolveViewDays(fallback: number): number { - const raw = getCookie("setting_viewDays"); - const saved = raw === null ? NaN : Number(raw); - if (Number.isInteger(saved) && saved >= 1 && saved <= 31) return saved; - const days = clampViewDays(fallback || 7); - if (typeof window === "undefined") return days; - if (window.innerWidth <= 768) return window.innerHeight > window.innerWidth ? 1 : 3; - return window.innerWidth <= 1024 ? Math.min(days, 5) : days; -} - -function readDeviceViewCookie(): Record> { - const raw = getCookie("device_view_settings"); - if (!raw) return {}; - try { return JSON.parse(raw); } catch { return {}; } -} - -function writeDeviceViewCookie(settings: Record>) { - setCookie("device_view_settings", JSON.stringify(settings)); -} @@ -175,20 +140,7 @@ function writeDeviceViewCookie(settings: Record>) { // Time grid configuration options -const DEFAULT_SOMEDAY_SLOT_COUNT = 5; -const getSomedaySlotCount = (tasks: Task[]) => { - const maxIdx = tasks.reduce((max, t) => { - if (t.somedaySlotIndex !== null && t.somedaySlotIndex !== undefined) { - return Math.max(max, t.somedaySlotIndex); - } - return max; - }, -1); - // Add 1 extra slot if more than 4 tasks exist, or at least 5 slots total. - // "add 5 rows and then when 4 are taken add another row" - // Let's ensure there's always at least one empty slot at the bottom. - return Math.max(DEFAULT_SOMEDAY_SLOT_COUNT, maxIdx + 2); -}; // Helper to load Google Fonts const useGoogleFonts = (fonts: string[]) => { @@ -233,54 +185,8 @@ import { // Check if an event is an all-day event // Defined outside component to avoid stale closure issues in useCallbacks -const isAllDayEvent = (event: CalendarEvent): boolean => { - if (!event.startTime) return false; - - // Date-only format (YYYY-MM-DD) - if (!event.startTime.includes("T")) return true; - - const start = parseCalendarDate(event.startTime); - const end = parseCalendarDate(event.endTime); - const durationHours = (end.getTime() - start.getTime()) / (1000 * 60 * 60); - - // Check if strictly midnight to midnight in local time - const isLocalMidnight = start.getHours() === 0 && start.getMinutes() === 0; - - // Check if UTC midnight (common for API-converted date strings) - const isUTCMidnight = - start.getUTCHours() === 0 && start.getUTCMinutes() === 0; - - // If it's effectively 24h+ and starts at midnight (local or UTC), treat as all-day - return durationHours >= 24 && (isLocalMidnight || isUTCMidnight); -}; // Helper to invert colors for dark mode -function invertColor(hex: string): string { - if (!hex) return hex; - let color = hex.startsWith("#") ? hex.slice(1) : hex; - if (color.length === 3) { - color = color - .split("") - .map((c) => c + c) - .join(""); - } - if (color.length !== 6) return hex; - - try { - const r = (255 - parseInt(color.slice(0, 2), 16)) - .toString(16) - .padStart(2, "0"); - const g = (255 - parseInt(color.slice(2, 4), 16)) - .toString(16) - .padStart(2, "0"); - const b = (255 - parseInt(color.slice(4, 6), 16)) - .toString(16) - .padStart(2, "0"); - return `#${r}${g}${b}`; - } catch (e) { - return hex; - } -} // Helper to lighten color for dark mode function adjustColorForDarkMode(hex: string, isDarkMode: boolean): string { diff --git a/src/lib/weekly/device-settings.ts b/src/lib/weekly/device-settings.ts new file mode 100644 index 0000000..ac792a0 --- /dev/null +++ b/src/lib/weekly/device-settings.ts @@ -0,0 +1,52 @@ +/** + * Geräteabhängige Einstellungen der Wochenansicht. + * + * Diese Schlüssel werden bewusst NUR im Cookie gehalten, nicht in der Datenbank: Jedes Gerät + * behält so seine eigenen Werte (ein Handy will andere Spaltenzahlen als ein 34-Zoll-Monitor). + * Lagen bis v1.117.7 in WeeklyView.tsx. + */ + +// Cookie helpers for per-device settings +// Keys here bypass the DB and save/load from cookie only — each device keeps its own value +export const DEVICE_SETTINGS_KEYS = ["viewDays", "cellDuration", "startHour", "endHour", "fontSize", "showSubHourSlots"]; + +// Per-view toggle keys that are also device-specific (sidebar eye toggles) +export const DEVICE_VIEW_SETTINGS_KEYS = ["showSomeday", "showAllDayEvents", "showTaskCheckboxes", "showProjectIcons", "showPriorityIcons", "weatherEnabled"] as const; +export type DeviceViewSettingKey = typeof DEVICE_VIEW_SETTINGS_KEYS[number]; + +// Settings that save to DB (cross-device default) AND to cookie (device override wins on load) +export const DEVICE_ALSO_COOKIE_KEYS = ["viewStyle", "showTimeGrid", "startDayOffset"]; +export const VIEW_SETTINGS_PROFILE_KEYS = ["menuPosition", "showHeaderControls"]; + +export function getCookie(name: string): string | null { + if (typeof document === "undefined") return null; + const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); + return match ? decodeURIComponent(match[1]) : null; +} + +export function setCookie(name: string, value: string, days: number = 365) { + if (typeof document === "undefined") return; + const expires = new Date(Date.now() + days * 864e5).toUTCString(); + document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`; +} + +// Explicit browser choices take precedence over responsive defaults on every load. +export function resolveViewDays(fallback: number): number { + const raw = getCookie("setting_viewDays"); + const saved = raw === null ? NaN : Number(raw); + if (Number.isInteger(saved) && saved >= 1 && saved <= 31) return saved; + const days = clampViewDays(fallback || 7); + if (typeof window === "undefined") return days; + if (window.innerWidth <= 768) return window.innerHeight > window.innerWidth ? 1 : 3; + return window.innerWidth <= 1024 ? Math.min(days, 5) : days; +} + +export function readDeviceViewCookie(): Record> { + const raw = getCookie("device_view_settings"); + if (!raw) return {}; + try { return JSON.parse(raw); } catch { return {}; } +} + +export function writeDeviceViewCookie(settings: Record>) { + setCookie("device_view_settings", JSON.stringify(settings)); +} diff --git a/src/lib/weekly/format.ts b/src/lib/weekly/format.ts new file mode 100644 index 0000000..3239b1a --- /dev/null +++ b/src/lib/weekly/format.ts @@ -0,0 +1,71 @@ +import type { Task, CalendarEvent } from "@/lib/weekly/types"; + +/** + * Darstellungs-Helfer der Wochenansicht: Text, Farben, Ganztags-Erkennung, Slot-Anzahl. + * Reine Funktionen ohne Komponenten-State — lagen bis v1.117.7 in WeeklyView.tsx. + */ + +export const stripHtml = (html: string) => html.replace(/<[^>]*>/g, '').trim(); + +export const DEFAULT_SOMEDAY_SLOT_COUNT = 5; + +export const getSomedaySlotCount = (tasks: Task[]) => { + const maxIdx = tasks.reduce((max, t) => { + if (t.somedaySlotIndex !== null && t.somedaySlotIndex !== undefined) { + return Math.max(max, t.somedaySlotIndex); + } + return max; + }, -1); + // Add 1 extra slot if more than 4 tasks exist, or at least 5 slots total. + // "add 5 rows and then when 4 are taken add another row" + // Let's ensure there's always at least one empty slot at the bottom. + return Math.max(DEFAULT_SOMEDAY_SLOT_COUNT, maxIdx + 2); +}; + +export const isAllDayEvent = (event: CalendarEvent): boolean => { + if (!event.startTime) return false; + + // Date-only format (YYYY-MM-DD) + if (!event.startTime.includes("T")) return true; + + const start = parseCalendarDate(event.startTime); + const end = parseCalendarDate(event.endTime); + const durationHours = (end.getTime() - start.getTime()) / (1000 * 60 * 60); + + // Check if strictly midnight to midnight in local time + const isLocalMidnight = start.getHours() === 0 && start.getMinutes() === 0; + + // Check if UTC midnight (common for API-converted date strings) + const isUTCMidnight = + start.getUTCHours() === 0 && start.getUTCMinutes() === 0; + + // If it's effectively 24h+ and starts at midnight (local or UTC), treat as all-day + return durationHours >= 24 && (isLocalMidnight || isUTCMidnight); +}; + +export function invertColor(hex: string): string { + if (!hex) return hex; + let color = hex.startsWith("#") ? hex.slice(1) : hex; + if (color.length === 3) { + color = color + .split("") + .map((c) => c + c) + .join(""); + } + if (color.length !== 6) return hex; + + try { + const r = (255 - parseInt(color.slice(0, 2), 16)) + .toString(16) + .padStart(2, "0"); + const g = (255 - parseInt(color.slice(2, 4), 16)) + .toString(16) + .padStart(2, "0"); + const b = (255 - parseInt(color.slice(4, 6), 16)) + .toString(16) + .padStart(2, "0"); + return `#${r}${g}${b}`; + } catch (e) { + return hex; + } +}