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 <noreply@anthropic.com>
This commit is contained in:
parent
c303e1cf2b
commit
cd59c02995
13
CHANGELOG.md
13
CHANGELOG.md
@ -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
17
TODO.md
@ -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
4
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
52
src/lib/weekly/device-settings.ts
Normal file
52
src/lib/weekly/device-settings.ts
Normal 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
71
src/lib/weekly/format.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user