refactor: Geraeteeinstellungen und Formatierungs-Helfer auslagern
Some checks are pending
Playwright Tests / test (push) Waiting to run
Test and SonarQube / quality (push) Waiting to run

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 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-07 09:11:31 +02:00
parent c303e1cf2b
commit cd59c02995
7 changed files with 162 additions and 111 deletions

View File

@ -2,6 +2,19 @@
All notable changes to this project are documented in this file. 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 ## [1.117.6] - 2026-09-07
### Changed ### Changed

17
TODO.md
View File

@ -128,7 +128,7 @@ Betroffen sind genau zwei Dateien:
| Datei | Zeilen | | 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 | | `src/components/SettingsSidebar.tsx` | 5.445 |
**Voraussetzung:** C1 muss laufen — ohne Typecheck ist ein Refactoring dieser Größe **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 - [x] Typen nach `src/lib/weekly/types.ts` (7 Typen, 97 Zeilen) — Build verifiziert
- [ ] `src/components/weekly/` anlegen - [ ] `src/components/weekly/` anlegen
- [x] Datums-/Zeitraster-Helfer nach `src/lib/weekly/date-utils.ts` (13 Funktionen, 202 Zeilen) - [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` - [x] Cookie-/Geräteeinstellungen nach `src/lib/weekly/device-settings.ts`
- [ ] Formatierungs-Helfer (`stripHtml`, `invertColor`, `isAllDayEvent`) auslagern - [x] Formatierungs-Helfer nach `src/lib/weekly/format.ts`
- [ ] `getWeekNumber` existiert **dreimal** im Projekt (WeeklyView, `WeekPrintModal.tsx`, - [ ] `getWeekNumber` existiert **dreimal** im Projekt (WeeklyView, `WeekPrintModal.tsx`,
`api/user/export-week-view/route.ts`) — Duplikate gegen `date-utils.ts` ersetzen, `api/user/export-week-view/route.ts`) — Duplikate gegen `date-utils.ts` ersetzen,
vorher die drei Implementierungen auf Gleichheit prüfen 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/` - [ ] Gemeinsame Bausteine (Farbwähler, Schalterzeile) in `settings/controls/`
**Vorgehen:** In kleinen Schritten, jeder Schritt ein eigener Commit, nach jedem Schritt **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 **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. Anfang September saß genau dort und war nur deshalb schwer zu finden.

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.117.6", "version": "1.117.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.117.6", "version": "1.117.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@auth/prisma-adapter": "^2.11.1", "@auth/prisma-adapter": "^2.11.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "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", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -98,7 +98,16 @@ import {
SlidersHorizontal, SlidersHorizontal,
} from "lucide-react"; } 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 // Types
import UserMenu from "./UserMenu"; import UserMenu from "./UserMenu";
@ -124,50 +133,6 @@ const SettingsSidebar = dynamic(() => import("./SettingsSidebar"), { ssr: false
const RichTextEditor = dynamic(() => import("./RichTextEditor"), { ssr: false }); const RichTextEditor = dynamic(() => import("./RichTextEditor"), { ssr: false });
const PriorityView = dynamic(() => import("./PriorityView"), { 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<string, Record<string, any>> {
const raw = getCookie("device_view_settings");
if (!raw) return {};
try { return JSON.parse(raw); } catch { return {}; }
}
function writeDeviceViewCookie(settings: Record<string, Record<string, any>>) {
setCookie("device_view_settings", JSON.stringify(settings));
}
@ -175,20 +140,7 @@ function writeDeviceViewCookie(settings: Record<string, Record<string, any>>) {
// Time grid configuration options // 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 // Helper to load Google Fonts
const useGoogleFonts = (fonts: string[]) => { const useGoogleFonts = (fonts: string[]) => {
@ -233,54 +185,8 @@ import {
// Check if an event is an all-day event // Check if an event is an all-day event
// Defined outside component to avoid stale closure issues in useCallbacks // 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 // 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 // Helper to lighten color for dark mode
function adjustColorForDarkMode(hex: string, isDarkMode: boolean): string { function adjustColorForDarkMode(hex: string, isDarkMode: boolean): string {

View File

@ -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<string, Record<string, any>> {
const raw = getCookie("device_view_settings");
if (!raw) return {};
try { return JSON.parse(raw); } catch { return {}; }
}
export function writeDeviceViewCookie(settings: Record<string, Record<string, any>>) {
setCookie("device_view_settings", JSON.stringify(settings));
}

71
src/lib/weekly/format.ts Normal file
View File

@ -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;
}
}