From ebfe911b81c14d389688612bb1757fa80d920429 Mon Sep 17 00:00:00 2001 From: mARTin Date: Sat, 14 Mar 2026 18:32:09 +0100 Subject: [PATCH] fix: align time column header, calendar events, all-day events, and swap view icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix time-column-header height to dynamically match weekly-day-header - Fix all-day events column alignment (width 50px → 55px to match time column) - Fix calendar events overflowing weekday columns (remove negative left/right) - Swap Simple/Calendar view icons in header switcher - Uncomment headerCustomFormat in API select for instant toggle - Add response error checking in updateTaskFields for kanban persistence - Add font/view dependencies to header height ResizeObserver v1.33.2 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- prisma/schema.prisma | 1 + src/app/api/user/profile/route.ts | 5 +- src/app/globals.css | 38 +- src/components/CalendarEventModal.tsx | 434 ++++++++---- src/components/WeeklyView.tsx | 922 ++++++++++---------------- 6 files changed, 662 insertions(+), 740 deletions(-) diff --git a/package.json b/package.json index 030aaa8..b51efc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.33.1", + "version": "1.33.2", "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/prisma/schema.prisma b/prisma/schema.prisma index a783c75..479e9f4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -79,6 +79,7 @@ model User { dateAlignment String @default("center") dateVerticalAlign String? @default("middle") headerDisplay String? @default("kw") + headerCustomFormat String? @default("KW WW | YYYY") hourLabelFormat String @default("short") showSubHourSlots Boolean @default(true) allDayPosition String @default("above") diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts index 35fb5b4..eda10f2 100644 --- a/src/app/api/user/profile/route.ts +++ b/src/app/api/user/profile/route.ts @@ -46,6 +46,7 @@ export async function GET(request: NextRequest) { mobileDateLayout: true, dateVerticalAlign: true, headerDisplay: true, + headerCustomFormat: true, headlineFont: true, headlineFontSize: true, headlineFontWeight: true, @@ -130,7 +131,7 @@ export async function PATCH(request: NextRequest) { weekdayColor, dateColor, taskColor, todayHighlightColor, pastDayColor, goalFallbackType, goalDefaultSentence, goalFontFamily, goalFontSize, goalFontWeight, goalScope, - dateLayout, mobileDateLayout, dateVerticalAlign, headerDisplay, + dateLayout, mobileDateLayout, dateVerticalAlign, headerDisplay, headerCustomFormat, hourLabelFormat, showSubHourSlots, allDayPosition, cwFontFamily, cwFontSize, cwFontWeight, cwColor, yearFontFamily, yearFontSize, yearFontWeight, yearColor, @@ -196,6 +197,7 @@ export async function PATCH(request: NextRequest) { ...(mobileDateLayout !== undefined && { mobileDateLayout }), ...(dateVerticalAlign !== undefined && { dateVerticalAlign }), ...(headerDisplay !== undefined && { headerDisplay }), + ...(headerCustomFormat !== undefined && { headerCustomFormat }), ...(hourLabelFormat !== undefined && { hourLabelFormat }), ...(showSubHourSlots !== undefined && { showSubHourSlots }), ...(allDayPosition !== undefined && { allDayPosition }), @@ -289,6 +291,7 @@ export async function PATCH(request: NextRequest) { mobileDateLayout: true, dateVerticalAlign: true, headerDisplay: true, + headerCustomFormat: true, cwFontFamily: true, cwFontSize: true, cwFontWeight: true, diff --git a/src/app/globals.css b/src/app/globals.css index 094f5a3..ca8da9f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2324,15 +2324,15 @@ h3 { /* Side Navigation Arrows (hover overlays) */ .side-nav { - position: sticky; - top: 50px; /* Below header height if needed, or 0 */ - height: 0; /* Let it be a point of origin */ + position: absolute; + top: 0; + height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; - padding: 0.35rem 0.15rem; - gap: 0.1rem; + padding: 2px; + gap: 2px; z-index: 30; pointer-events: none; /* Let clicks pass through to grid except buttons */ } @@ -2352,6 +2352,7 @@ h3 { .side-nav-left { left: 0; + width: 100%; } .side-nav-right { @@ -2372,43 +2373,50 @@ h3 { } .side-nav-btn:hover { - background: var(--weekly-hover-bg, rgba(0, 0, 0, 0.06)); + background: var(--weekly-bg); /* Use background over hover to hide header text */ + box-shadow: 0 1px 3px rgba(0,0,0,0.1); color: var(--weekly-text, #333); } /* Time Column (Hours) */ .time-column { - width: 50px; + width: 55px; /* Slightly wider for labels */ flex-shrink: 0; border-right: 1px solid var(--weekly-border); background: var(--weekly-bg); display: flex; flex-direction: column; + /* Flush to left */ + margin-left: -1px; } .time-column-header { - padding: 1rem 0.5rem 0.75rem; - min-height: 50px !important; + padding: 1rem 1rem 0.75rem; /* Match weekly-day-header padding */ + min-height: 50px; /* Match weekly-day-header min-height */ box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; } .time-column-slots { flex: 1; overflow-y: visible; /* Unify with parent scroll */ position: relative; - padding-top: 0.4rem; + padding-top: 0; /* Flush with header */ } .time-slot-label { display: flex; - align-items: flex-start; /* Aligns to top so translateY(-50%) centers on the line */ + align-items: flex-start; justify-content: flex-end; padding: 0 0.5rem; font-size: 0.65rem; color: var(--weekly-text-light); box-sizing: border-box; position: relative; + z-index: 10; } .time-slot-label.hour-start { @@ -2444,7 +2452,7 @@ h3 { flex: 1; overflow: visible; /* Changed from auto to unify scroll */ overflow-x: visible; - padding-top: 0.4rem; /* Aligned with .time-column-slots */ + padding-top: 0; /* Flush with header */ } .time-slot { @@ -2549,8 +2557,9 @@ h3 { border-radius: 0 4px 4px 0; margin: 1px 0; overflow: hidden; - left: -10px; - right: -15px; + left: 0; + right: 0; + box-sizing: border-box; } .time-slot-event .event-title-row { @@ -2666,6 +2675,7 @@ h3 { min-height: 28px; border-right: 1px solid var(--weekly-border); overflow-y: auto; + overflow-x: hidden; scrollbar-width: none; /* Firefox */ -ms-overflow-style: none; /* IE/Edge */ } diff --git a/src/components/CalendarEventModal.tsx b/src/components/CalendarEventModal.tsx index e53b7f8..fff8d31 100644 --- a/src/components/CalendarEventModal.tsx +++ b/src/components/CalendarEventModal.tsx @@ -1,4 +1,8 @@ -import React, { useState, useEffect, lazy, Suspense } from 'react'; +import React, { useState, useEffect, lazy, Suspense, useRef } from 'react'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faGoogle, faApple, faMicrosoft } from '@fortawesome/free-brands-svg-icons'; +import { faServer } from '@fortawesome/free-solid-svg-icons'; const RichTextEditor = lazy(() => import('./RichTextEditor')); @@ -78,6 +82,31 @@ export default function CalendarEventModal({ const [isSaving, setIsSaving] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [error, setError] = useState(''); + const [isCalendarSelectorOpen, setIsCalendarSelectorOpen] = useState(false); + const calendarSelectorRef = useRef(null); + + // Close dropdown on outside click + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (calendarSelectorRef.current && !calendarSelectorRef.current.contains(event.target as Node)) { + setIsCalendarSelectorOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const selectedCal = availableCalendars.find((c: any) => c.id === calendarId); + const supportsURL = selectedCal && (selectedCal.provider !== 'google' && selectedCal.provider !== 'outlook'); + const getProviderIcon = (provider: string) => { + switch (provider) { + case 'google': return ; + case 'apple': return ; + case 'outlook': return ; + case 'synology': return ; + default: return null; + } + }; const handleSubmit = async () => { if (!title.trim()) { @@ -155,186 +184,313 @@ export default function CalendarEventModal({ } }; + return (
-
e.stopPropagation()} style={{ maxWidth: '500px' }}> -

- {event ? 'Edit Event' : 'New Event'} -

+
e.stopPropagation()} style={{ + maxWidth: '450px', + padding: '20px', + borderRadius: '12px', + boxShadow: '0 10px 25px rgba(0,0,0,0.15)', + border: '1px solid var(--weekly-border)' + }}> + {error &&
{error}
} - {error &&
{error}
} +
+ {/* Title Input */} + setTitle(e.target.value)} + placeholder="New Event" + autoFocus + style={{ + width: '100%', + padding: '12px 16px', + fontSize: '1.25rem', + fontWeight: 600, + border: 'none', + borderBottom: '1px solid var(--weekly-border)', + borderRadius: '0', + background: 'transparent', + outline: 'none', + marginBottom: '4px' + }} + /> -
+ {/* Location Input */} + setLocation(e.target.value)} + placeholder="Location or Video Call" + style={{ + width: '100%', + padding: '8px 16px', + border: 'none', + borderBottom: '1px solid var(--weekly-border)', + borderRadius: '0', + background: 'transparent', + outline: 'none', + fontSize: '0.95rem' + }} + /> - {/* Title */} -
- + {/* Calendar row: Label + Custom Selector */} +
+ Calendar + +
+ + + {isCalendarSelectorOpen && ( +
+ {availableCalendars.map((cal: any) => ( +
{ + setCalendarId(cal.id); + setIsCalendarSelectorOpen(false); + }} + style={{ + display: 'flex', + alignItems: 'center', + gap: '10px', + padding: '8px 12px', + borderRadius: '6px', + cursor: 'pointer', + fontSize: '0.9rem', + color: 'var(--weekly-text)', + backgroundColor: calendarId === cal.id ? 'var(--weekly-selection, rgba(59, 130, 246, 0.1))' : 'transparent', + transition: 'background 0.2s', + textAlign: 'left' + }} + onMouseEnter={(e) => e.currentTarget.style.backgroundColor = 'var(--weekly-hover, rgba(0,0,0,0.05))'} + onMouseLeave={(e) => e.currentTarget.style.backgroundColor = calendarId === cal.id ? 'var(--weekly-selection, rgba(59, 130, 246, 0.1))' : 'transparent'} + > +
+ + {cal.summary || cal.title} + + + {getProviderIcon(cal.provider)} + +
+ ))} +
+ )} +
+
+ + {/* All Day row */} +
+ All Day setTitle(e.target.value)} - placeholder="Event Title" - autoFocus - style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '1rem' }} + type="checkbox" + checked={allDay} + onChange={e => setAllDay(e.target.checked)} + style={{ width: '18px', height: '18px', cursor: 'pointer' }} />
- {/* Calendar Selection */} -
- - -
- - {/* All Day Toggle */} -
- -
- - {/* Date/Time */} -
-
- + {/* Date/Time rows */} +
+
+ Starts handleStartDateChange(e.target.value)} - style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }} + style={{ + padding: '4px 8px', + border: 'none', + borderRadius: '6px', + background: 'var(--weekly-bg-secondary, #f3f4f6)', + fontSize: '0.9rem', + textAlign: 'center' + }} />
-
- +
+ Ends setEndDate(new Date(e.target.value))} - style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }} + style={{ + padding: '4px 8px', + border: 'none', + borderRadius: '6px', + background: 'var(--weekly-bg-secondary, #f3f4f6)', + fontSize: '0.9rem', + textAlign: 'center' + }} />
- {/* Recurrence */} -
- - -
+ {/* Meta Fields Group */} +
+ {/* Recurrence */} +
+ Repeat + +
- {/* Location */} -
- - setLocation(e.target.value)} - placeholder="Location (optional)" - style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }} - /> -
+ {/* URL (Conditional) */} + {supportsURL && ( + setUrl(e.target.value)} + placeholder="URL" + style={{ + width: '100%', + padding: '6px 0', + border: 'none', + borderBottom: '1px solid var(--weekly-border)', + background: 'transparent', + fontSize: '0.9rem', + outline: 'none' + }} + /> + )} - {/* URL */} - {(() => { - const selectedCal = availableCalendars.find((c: any) => c.id === calendarId); - const isUrlDisabled = selectedCal && (selectedCal.provider === 'google' || selectedCal.provider === 'outlook'); - return ( -
- - setUrl(e.target.value)} - placeholder={isUrlDisabled ? 'Not supported by this calendar provider' : 'https://...'} - disabled={!!isUrlDisabled} - style={{ - width: '100%', - padding: '8px', - border: '1px solid #ddd', - borderRadius: '4px', - opacity: isUrlDisabled ? 0.5 : 1, - background: isUrlDisabled ? '#f5f5f5' : undefined, - cursor: isUrlDisabled ? 'not-allowed' : undefined, - }} + {/* Notes */} +
+ setDescription(e.target.value)} + placeholder="Notes" + rows={3} + style={{ width: '100%', padding: '8px 0', border: 'none', borderBottom: '1px solid var(--weekly-border)', background: 'transparent', resize: 'none', fontSize: '0.9rem', outline: 'none' }} /> -
- ); - })()} - - {/* Description - Rich text */} -
- - setDescription(e.target.value)} - placeholder="Notes..." - rows={3} - style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px', resize: 'vertical' }} - /> - }> - - + }> + + +
-
-
-
+
+
{event && onDelete && ( )} + {!event && ( + + )}
-
- - + )} +
diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index c737185..e5c48b3 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -260,10 +260,11 @@ const translations: Record = { noStage: "No stage", headerDisplay: "Header Display", headerDisplayKW: "Calendar Week (KW)", - headerDisplayMonth: "Month Name", - headerDisplayMonthYear: "Month & Year", - headerDisplayDate: "Full Date", - headerDisplayCustom: "Custom", + headerDisplayMonth: "Month Name - March", + headerDisplayMonthYear: "Month & Year - March | 2026", + headerDisplayDate: "Full Date - 13.03.2026", + headerDisplayCustom: "Custom - Friday - 13. March", + headerDisplayNone: "None", headerCustomFormatLabel: "Format string (e.g. DD.MM.YYYY)", language: "Language", dateFormat: "Date Format", @@ -470,10 +471,11 @@ const translations: Record = { noStage: "Keine Phase", headerDisplay: "Kopfzeile", headerDisplayKW: "Kalenderwoche (KW)", - headerDisplayMonth: "Monatsname", - headerDisplayMonthYear: "Monat & Jahr", - headerDisplayDate: "Vollständiges Datum", - headerDisplayCustom: "Benutzerdefiniert", + headerDisplayMonth: "Monatsname - März", + headerDisplayMonthYear: "Monat & Jahr - März | 2026", + headerDisplayDate: "Vollständiges Datum - 13.03.2026", + headerDisplayCustom: "Benutzerdefiniert - Freitag - 13. März", + headerDisplayNone: "Nichts", headerCustomFormatLabel: "Format (z.B. DD.MM.YYYY)", listView: "Liste", notes: "Notizen", @@ -683,10 +685,11 @@ const translations: Record = { noStage: "Aucune étape", headerDisplay: "Affichage en-tête", headerDisplayKW: "Semaine calendaire (KW)", - headerDisplayMonth: "Nom du mois", - headerDisplayMonthYear: "Mois & Année", - headerDisplayDate: "Date complète", - headerDisplayCustom: "Personnalisé", + headerDisplayMonth: "Nom du mois - Mars", + headerDisplayMonthYear: "Mois & Année - Mars | 2026", + headerDisplayDate: "Date complète - 13.03.2026", + headerDisplayCustom: "Personnalisé - Vendredi - 13 Mars", + headerDisplayNone: "Aucun", headerCustomFormatLabel: "Format (ex: DD.MM.YYYY)", language: "Langue", dateFormat: "Format de date", @@ -894,10 +897,11 @@ const translations: Record = { noStage: "Sin etapa", headerDisplay: "Visualización de encabezado", headerDisplayKW: "Semana calendario (KW)", - headerDisplayMonth: "Nombre del mes", - headerDisplayMonthYear: "Mes y Año", - headerDisplayDate: "Fecha completa", - headerDisplayCustom: "Personalizado", + headerDisplayMonth: "Nombre del mes - Marzo", + headerDisplayMonthYear: "Mes y Año - Marzo | 2026", + headerDisplayDate: "Fecha completa - 13.03.2026", + headerDisplayCustom: "Personalizado - Viernes - 13 Marzo", + headerDisplayNone: "Ninguno", headerCustomFormatLabel: "Formato (ej. DD.MM.YYYY)", language: "Idioma", dateFormat: "Formato de fecha", @@ -1105,10 +1109,11 @@ const translations: Record = { noStage: "Nessuna fase", headerDisplay: "Visualizzazione intestazione", headerDisplayKW: "Settimana calendario (KW)", - headerDisplayMonth: "Nome del mese", - headerDisplayMonthYear: "Mese e Anno", - headerDisplayDate: "Data completa", - headerDisplayCustom: "Personalizzato", + headerDisplayMonth: "Nome del mese - Marzo", + headerDisplayMonthYear: "Mese e Anno - Marzo | 2026", + headerDisplayDate: "Data completa - 13.03.2026", + headerDisplayCustom: "Personalizzato - Venerdì - 13 Marzo", + headerDisplayNone: "Nessuno", headerCustomFormatLabel: "Formato (es. DD.MM.YYYY)", language: "Lingua", dateFormat: "Formato data", @@ -1437,32 +1442,37 @@ function getCWReferenceDate(days: Date[]): Date { // Format a custom header string using tokens function formatCustomHeader(format: string, days: Date[], language: string, t: any): string { if (!format) return ""; - const refDate = getCWReferenceDate(days); + + // Choose the reference date: if today is within the visible days, use today. + // Otherwise, use the standard CW reference date (start of week). const today = new Date(); + const isTodayInWeek = days.some(d => + d.getDate() === today.getDate() && + d.getMonth() === today.getMonth() && + d.getFullYear() === today.getFullYear() + ); + const refDate = isTodayInWeek ? today : getCWReferenceDate(days); - let result = format; + // Define token mappings + const tokens: Record = { + "YYYY": refDate.getFullYear().toString(), + "WW": getWeekNumber(refDate).toString().padStart(2, '0'), + "MMMM": refDate.toLocaleDateString(language, { month: 'long' }), + "MMM": refDate.toLocaleDateString(language, { month: 'short' }), + "MM": (refDate.getMonth() + 1).toString().padStart(2, '0'), + "M": (refDate.getMonth() + 1).toString(), + "DDDD": refDate.toLocaleDateString(language, { weekday: 'long' }), + "DDD": refDate.toLocaleDateString(language, { weekday: 'short' }), + "DD": refDate.getDate().toString().padStart(2, '0'), + "D": refDate.getDate().toString(), + "[TODAY]": today.toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' }) + }; + + // Single-pass replacement using regex to avoid nested replacements (e.g. M in MMMM) + // Standalone 'W' removed to allow literal 'W' (like in 'KW') + const regex = /\[TODAY\]|YYYY|WW|MMMM|MMM|MM|M|DDDD|DDD|DD|D/g; - // [TODAY] token - result = result.replace(/\[TODAY\]/g, today.toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' })); - - // YYYY - result = result.replace(/YYYY/g, refDate.getFullYear().toString()); - - // WW (ISO Week) - result = result.replace(/WW/g, getWeekNumber(refDate).toString().padStart(2, '0')); - result = result.replace(/W/g, getWeekNumber(refDate).toString()); - - // MMMM (Month Name Long) - result = result.replace(/MMMM/g, refDate.toLocaleDateString(language, { month: 'long' })); - result = result.replace(/MMM/g, refDate.toLocaleDateString(language, { month: 'short' })); - result = result.replace(/MM/g, (refDate.getMonth() + 1).toString().padStart(2, '0')); - result = result.replace(/M/g, (refDate.getMonth() + 1).toString()); - - // DD (Day) - result = result.replace(/DD/g, refDate.getDate().toString().padStart(2, '0')); - result = result.replace(/D/g, refDate.getDate().toString()); - - return result; + return format.replace(regex, (match) => tokens[match] || match); } // Check if an event is an all-day event @@ -1767,88 +1777,7 @@ export default function WeeklyView() { Record >({}); const [isVisible, setIsVisible] = useState(false); - const [profile, setProfile] = useState<{ - name: string; - email: string; - timezone: string; - autoRolling?: boolean; - protectEventTimes?: boolean; - language?: string; - dateFormat?: string; - timeFormat?: string; - startHour?: number; - endHour?: number; - focusTimerDuration?: number; - focusBreakDuration?: number; - showTimeGrid?: boolean; - cellDuration?: number; - viewStyle?: string; - fontSize?: "S" | "M" | "L"; - showNextTask?: boolean; - showSomeday?: boolean; - showAllDayEvents?: boolean; - showSchedule?: boolean; - headlineFont?: string; - headlineFontSize?: string; - headlineFontWeight?: string; - dateFontFamily?: string; - dateFontSize?: string; - dateFontWeight?: string; - timeTaskFontFamily?: string; - timeTaskFontSize?: string; - timeTaskFontWeight?: string; - bodyFont?: string; - taskFontFamily?: string; - taskFontSize?: string; - taskFontWeight?: string; - eventFontFamily?: string; - eventFontSize?: string; - eventFontWeight?: string; - fontWeight?: string; - weekendColorSat?: string; - weekendColorSun?: string; - weekdayColor?: string; - dateColor?: string; - taskColor?: string; - todayHighlightColor?: string; - pastDayColor?: string; - goalFallbackType?: "quote" | "next_todo" | "default"; - quoteSourceUrl?: string; - goalDefaultSentence?: string; - goalFontFamily?: string; - goalFontSize?: string; - goalFontWeight?: string; - goalScope?: "week" | "day"; - dateLayout?: "above" | "below" | "left" | "right" | "hidden"; - mobileDateLayout?: "above" | "below" | "left" | "right" | "hidden"; - dateAlignment?: "left" | "center" | "right" | "tight"; - hourLabelFormat?: "short" | "full"; - showSubHourSlots?: boolean; - allDayPosition?: "above" | "below"; - cwFontFamily?: string; - cwFontSize?: string; - cwFontWeight?: string; - cwColor?: string; - yearFontFamily?: string; - yearFontSize?: string; - yearFontWeight?: string; - yearColor?: string; - dayHeaderGap?: string; - showTaskCheckboxes?: boolean; - quoteSourceUrls?: string[]; - quoteLanguages?: string[]; - startDayOffset?: number; - id?: string; - accountNumber?: number; - weekdayFormat?: "long" | "short" | "narrow" | "custom"; - weekdayCase?: "normal" | "capitalize" | "uppercase"; - customWeekdayNames?: string; - dateVerticalAlign?: "top" | "middle" | "bottom"; - headerDisplay?: "kw" | "month" | "month_year" | "date" | "custom"; - headerCustomFormat?: string; - lightTheme?: any; - darkTheme?: any; - }>({ + const [profile, setProfile] = useState({ name: session?.user?.name || "", email: session?.user?.email || "", timezone: "UTC", @@ -2079,21 +2008,86 @@ export default function WeeklyView() { return () => window.removeEventListener("resize", check); }, []); - // Close mobile menu on outside click - useEffect(() => { - if (!showMobileMenu) return; - const handler = (e: MouseEvent) => { - if (mobileMenuRef.current && !mobileMenuRef.current.contains(e.target as Node)) { - setShowMobileMenu(false); + const profileLoadedRef = useRef(false); + const autoSaveTimerRef = useRef(null); + + const fetchProfile = async () => { + try { + const res = await fetch("/api/user/profile"); + if (res.ok) { + const data = await res.json(); + if (data && data.user) { + const profileData = data.user; + setProfile(profileData); + + // Sync individual states to profile data + if (profileData.viewStyle) setViewStyle(profileData.viewStyle); + if (profileData.viewDays) setViewDays(profileData.viewDays); + if (profileData.showTimeGrid !== undefined) setShowTimeGrid(profileData.showTimeGrid); + if (profileData.showSomeday !== undefined) setShowSomeday(profileData.showSomeday); + if (profileData.showAllDayEvents !== undefined) setShowAllDay(profileData.showAllDayEvents); + if (profileData.showSchedule !== undefined) setShowSchedule(profileData.showSchedule); + if (profileData.cellDuration) setCellDuration(profileData.cellDuration); + if (profileData.language) setLanguage(profileData.language); + if (profileData.dateFormat) setDateFormat(profileData.dateFormat); + if (profileData.timeFormat) setTimeFormat(profileData.timeFormat); + if (profileData.startHour !== undefined) setStartHour(profileData.startHour); + if (profileData.endHour !== undefined) setEndHour(profileData.endHour); + if (profileData.fontSize) setFontSize(profileData.fontSize); + if (profileData.showNextTask !== undefined) setShowNextTask(profileData.showNextTask); + if (profileData.protectEventTimes !== undefined) setProtectEventTimes(profileData.protectEventTimes); + if (profileData.headlineFont) setHeadlineFont(profileData.headlineFont); + if (profileData.headlineFontSize) setHeadlineFontSize(profileData.headlineFontSize); + if (profileData.headlineFontWeight) setHeadlineFontWeight(profileData.headlineFontWeight); + if (profileData.dateFontFamily) setDateFontFamily(profileData.dateFontFamily); + if (profileData.dateFontSize) setDateFontSize(profileData.dateFontSize); + if (profileData.dateFontWeight) setDateFontWeight(profileData.dateFontWeight); + if (profileData.timeTaskFontFamily) setTimeTaskFontFamily(profileData.timeTaskFontFamily); + if (profileData.timeTaskFontSize) setTimeTaskFontSize(profileData.timeTaskFontSize); + if (profileData.timeTaskFontWeight) setTimeTaskFontWeight(profileData.timeTaskFontWeight); + if (profileData.bodyFont) setBodyFont(profileData.bodyFont); + if (profileData.taskFontFamily) setTaskFontFamily(profileData.taskFontFamily); + if (profileData.taskFontSize) setTaskFontSize(profileData.taskFontSize); + if (profileData.taskFontWeight) setTaskFontWeight(profileData.taskFontWeight); + if (profileData.fontWeight) setFontWeight(profileData.fontWeight); + if (profileData.weekendColorSat) setWeekendColorSat(profileData.weekendColorSat); + if (profileData.weekendColorSun) setWeekendColorSun(profileData.weekendColorSun); + if (profileData.hourLabelFormat) setHourLabelFormat(profileData.hourLabelFormat); + if (profileData.showSubHourSlots !== undefined) setShowSubHourSlots(profileData.showSubHourSlots); + if (profileData.allDayPosition) setAllDayPosition(profileData.allDayPosition); + } } - }; - document.addEventListener("mousedown", handler); - document.addEventListener("touchstart", handler as EventListener); + } catch (err) { + console.error("Failed to fetch profile:", err); + } finally { + profileLoadedRef.current = true; + setIsLoading(false); + } + }; + + useEffect(() => { + fetchProfile(); + }, []); + + useEffect(() => { + if (!profileLoadedRef.current) return; + if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); + autoSaveTimerRef.current = setTimeout(async () => { + try { + await fetch("/api/user/profile", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(profile), + }); + console.log("[SETTINGS] Auto-saved profile"); + } catch (err) { + console.error("[SETTINGS] Auto-save failed:", err); + } + }, 800); return () => { - document.removeEventListener("mousedown", handler); - document.removeEventListener("touchstart", handler as EventListener); + if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); }; - }, [showMobileMenu]); + }, [profile]); // Auto-focus FAB bottom sheet textarea useEffect(() => { @@ -2129,21 +2123,48 @@ export default function WeeklyView() { const isScrollSyncing = useRef(false); const isInitialScrollDone = useRef(false); const intendedScrollTop = useRef(null); + const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState(65); const dayHeaderRef = useRef(null); const somedayGridRef = useRef(null); + + useEffect(() => { + const updateHeight = () => { + if (dayHeaderRef.current) { + const height = dayHeaderRef.current.offsetHeight; + // Allow some tolerance to avoid infinite loops across various browsers + if (height > 0 && Math.abs(height - measuredHeaderHeight) > 1) { + setMeasuredHeaderHeight(height); + } + } + }; + + // Initial measurement + updateHeight(); + + // Measurement after a short delay for layout stabilization + const timer = setTimeout(updateHeight, 800); + + // Also track window resize + window.addEventListener('resize', updateHeight); + + // ResizeObserver for more robust tracking of layout shifts + let resizeObserver: ResizeObserver | null = null; + if (typeof window !== 'undefined' && 'ResizeObserver' in window && dayHeaderRef.current) { + resizeObserver = new ResizeObserver(updateHeight); + resizeObserver.observe(dayHeaderRef.current); + } + + return () => { + clearTimeout(timer); + window.removeEventListener('resize', updateHeight); + if (resizeObserver) resizeObserver.disconnect(); + }; + }, [dayHeaderRef.current, cellDuration, viewDays, isMobile, profile.mobileDateLayout, profile.dateLayout, profile.dateAlignment, profile.dayHeaderGap, profile.headlineFontSize, profile.headlineFontWeight, profile.dateFontSize, profile.dateVerticalAlign, profile.headerDisplay, profile.weekdayFormat, viewStyle]); const somedaySectionRef = useRef(null); // Unified scroll sync handlers const handleTimeColumnScroll = (e: React.UIEvent) => { const scrollTop = e.currentTarget.scrollTop; - - // Detection for unexpected midnight reset during initial stabilization - if (intendedScrollTop.current !== null && scrollTop === 0 && !isInitialScrollDone.current) { - console.log(`[SCROLL] Detected unexpected reset to 0. Correcting to ${intendedScrollTop.current}px...`); - e.currentTarget.scrollTop = intendedScrollTop.current; - return; - } - if (isScrollSyncing.current) return; isScrollSyncing.current = true; @@ -2152,19 +2173,11 @@ export default function WeeklyView() { } setTimeout(() => { isScrollSyncing.current = false; - }, 100); + }, 50); }; const handleGridScroll = (e: React.UIEvent) => { const scrollTop = e.currentTarget.scrollTop; - - // Detection for unexpected midnight reset during initial stabilization - if (intendedScrollTop.current !== null && scrollTop === 0 && !isInitialScrollDone.current) { - console.log(`[SCROLL] Detected unexpected reset to 0. Correcting to ${intendedScrollTop.current}px...`); - e.currentTarget.scrollTop = intendedScrollTop.current; - return; - } - if (isScrollSyncing.current) return; isScrollSyncing.current = true; @@ -2173,29 +2186,36 @@ export default function WeeklyView() { } setTimeout(() => { isScrollSyncing.current = false; - }, 100); + }, 50); }; const jumpToHour = (hour: number) => { const slotsPerHour = 60 / cellDuration; const slotHeight = getSlotHeight(cellDuration); - // Offset relative to workingHoursStart because the grid now starts there - const relativeHour = Math.max(0, hour - workingHoursStart); - const scrollOffset = relativeHour * slotsPerHour * slotHeight; + const scrollOffset = hour * slotsPerHour * slotHeight; - console.log(`[SCROLL] Jumping to hour ${hour} (relative ${relativeHour}, offset ${scrollOffset}px)`); + console.log(`[SCROLL] Jumping to hour ${hour} (offset ${scrollOffset}px)`); + // Clear old stabilization + isInitialScrollDone.current = true; isScrollSyncing.current = true; intendedScrollTop.current = scrollOffset; - requestAnimationFrame(() => { - if (gridRef.current) gridRef.current.scrollTo({ top: scrollOffset, behavior: 'instant' }); - if (timeColumnRef.current) timeColumnRef.current.scrollTo({ top: scrollOffset, behavior: 'instant' }); - - setTimeout(() => { - isScrollSyncing.current = false; - }, 500); - }); + const perform = () => { + if (gridRef.current) gridRef.current.scrollTop = scrollOffset; + if (timeColumnRef.current) timeColumnRef.current.scrollTop = scrollOffset; + }; + + // Repeated enforcement + perform(); + requestAnimationFrame(perform); + setTimeout(perform, 50); + setTimeout(perform, 100); + setTimeout(perform, 250); + + setTimeout(() => { + isScrollSyncing.current = false; + }, 500); }; // Slot and Header height based on cell duration @@ -2210,6 +2230,7 @@ export default function WeeklyView() { }; const getHeaderHeight = (duration: number) => { + if (measuredHeaderHeight > 0) return measuredHeaderHeight; switch (duration) { case 15: return 65; case 30: return 55; @@ -2433,7 +2454,7 @@ export default function WeeklyView() { fetchCalendarEvents(); fetchMotivationalQuote(); } - }, [session, fetchMotivationalQuote]); + }, [session]); // Removed fetchMotivationalQuote from deps to avoid re-runs // Auto-open settings to calendar tab after OAuth redirect useEffect(() => { @@ -2558,34 +2579,37 @@ export default function WeeklyView() { } }, [currentWeekStart, session, fetchCalendarEvents]); - // Initial scroll to top (grid now starts at workingHoursStart) + // Initial scroll to preferred start hour useEffect(() => { if (!isLoading) { const performScroll = () => { - // Since the grid restricted to [workingHoursStart, workingHoursEnd], - // the top is already the start of the working hours. - const scrollOffset = 0; + const slotsPerHour = 60 / cellDuration; + const slotHeight = getSlotHeight(cellDuration); + const scrollOffset = workingHoursStart * slotsPerHour * slotHeight; + + console.log(`[SCROLL] Initial scroll to startHour ${workingHoursStart} (offset ${scrollOffset}px)`); + + // Force it directly + if (gridRef.current) gridRef.current.scrollTop = scrollOffset; + if (timeColumnRef.current) timeColumnRef.current.scrollTop = scrollOffset; - isScrollSyncing.current = true; intendedScrollTop.current = scrollOffset; - - requestAnimationFrame(() => { - const jump = (el: HTMLElement | null) => { - if (el) el.scrollTo({ top: scrollOffset, behavior: 'auto' }); - }; - - jump(gridRef.current); - jump(timeColumnRef.current); - setTimeout(() => { - isScrollSyncing.current = false; - isInitialScrollDone.current = true; - }, 500); - }); + // Repeatedly re-enforce for 2 seconds to fight browser auto-scroll restoration + const interval = setInterval(() => { + if (gridRef.current) gridRef.current.scrollTop = scrollOffset; + if (timeColumnRef.current) timeColumnRef.current.scrollTop = scrollOffset; + }, 50); + + setTimeout(() => { + clearInterval(interval); + isInitialScrollDone.current = true; + console.log(`[SCROLL] Stabilization period done. Grid scroll: ${gridRef.current?.scrollTop}`); + }, 2000); }; // Delay slightly to ensure layout is stable - const timer = setTimeout(performScroll, 300); + const timer = setTimeout(performScroll, 500); return () => clearTimeout(timer); } }, [isLoading, workingHoursStart, cellDuration]); @@ -2886,7 +2910,7 @@ export default function WeeklyView() { if (data.user.weekendColorSun) setWeekendColorSun(data.user.weekendColorSun); - setProfile((prev) => ({ + setProfile((prev: any) => ({ ...prev, ...data.user, name: data.user.name || prev.name, @@ -3975,11 +3999,15 @@ export default function WeeklyView() { ); try { - await fetch("/api/tasks", { + const res = await fetch("/api/tasks", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: taskId, ...fields }), }); + if (!res.ok) { + const errData = await res.json().catch(() => ({})); + console.error("Failed to update task fields:", res.status, errData); + } } catch (error) { console.error("Error updating task fields:", error); } @@ -4934,12 +4962,11 @@ export default function WeeklyView() { } }; - // Get time slots to display // Get time slots to display const visibleSlots = getTimeSlots( cellDuration, - workingHoursStart, - workingHoursEnd, + 0, + 24, ); const fontSizeScale = fontSize === "S" ? 0.85 : fontSize === "L" ? 1.15 : 1; @@ -5063,7 +5090,7 @@ export default function WeeklyView() { className="all-day-label-column" onClick={() => setIsAllDayExpanded(!isAllDayExpanded)} style={{ - width: "50px", + width: "55px", flexShrink: 0, display: "flex", flexDirection: "column", @@ -5073,6 +5100,7 @@ export default function WeeklyView() { borderRight: "1px solid var(--weekly-border)", padding: "2px 4px", gap: "0px", + marginLeft: "-1px", position: "relative", }} title={isAllDayExpanded ? "Collapse" : "Expand"} @@ -5152,6 +5180,15 @@ export default function WeeklyView() { key={event.id} className="all-day-event" title={`${event.calendarTitle}: ${event.title}`} + onClick={(e) => { + e.stopPropagation(); + if (event.editable) { + setCalendarEventModal({ + isOpen: true, + event: event, + }); + } + }} style={{ backgroundColor: event.calendarColor || "#3b82f6", @@ -5167,6 +5204,14 @@ export default function WeeklyView() { display: "flex", alignItems: "center", gap: "4px", + cursor: event.editable ? "pointer" : "default", + transition: "filter 0.1s ease", + }} + onMouseEnter={(e) => { + if (event.editable) e.currentTarget.style.filter = "brightness(0.9)"; + }} + onMouseLeave={(e) => { + if (event.editable) e.currentTarget.style.filter = "none"; }} > 📅 @@ -5512,8 +5557,19 @@ export default function WeeklyView() { ) : null}
- {profile.headerDisplay === "date" ? ( - {getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' })} + {profile.headerDisplay === "none" ? ( + null + ) : profile.headerDisplay === "date" ? ( + {(() => { + const today = new Date(); + const isTodayInWeek = getVisibleDays().some(d => + d.getDate() === today.getDate() && + d.getMonth() === today.getMonth() && + d.getFullYear() === today.getFullYear() + ); + const refDate = isTodayInWeek ? today : getCWReferenceDate(getVisibleDays()); + return refDate.toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' }); + })()} ) : profile.headerDisplay === "month_year" ? ( {getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { month: 'long', year: 'numeric' })} ) : profile.headerDisplay === "custom" ? ( @@ -5626,14 +5682,14 @@ export default function WeeklyView() { className={`p-1.5 rounded transition-colors ${viewStyle === "simple" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`} title={t.simpleView} > - + - -
- {/* Time Column */} {showTimeGrid && (
-
+ {/* Side Navigation Arrows (hover overlays) - now inside header */} +
+ + +
{/* Invisible structural match of day header to guarantee perfect height alignment */}
1 ? "week" : "day"} onScroll={handleGridScroll} style={{ - height: `${(workingHoursEnd - workingHoursStart) * (60 / cellDuration) * getSlotHeight(cellDuration) + getHeaderHeight(cellDuration)}px`, - maxHeight: `${(workingHoursEnd - workingHoursStart) * (60 / cellDuration) * getSlotHeight(cellDuration) + getHeaderHeight(cellDuration)}px`, + height: `${24 * (60 / cellDuration) * getSlotHeight(cellDuration) + getHeaderHeight(cellDuration)}px`, + maxHeight: `${24 * (60 / cellDuration) * getSlotHeight(cellDuration) + getHeaderHeight(cellDuration)}px`, flex: 1, alignSelf: "flex-start", overflowY: 'auto' @@ -6484,7 +6555,7 @@ export default function WeeklyView() { nowHour < 24 ) { const minutesSinceStart = - (nowHour - workingHoursStart) * 60 + nowMinute; + nowHour * 60 + nowMinute; const pixelsPerMinute = getSlotHeight(cellDuration) / cellDuration; const topPosition = @@ -6518,7 +6589,7 @@ export default function WeeklyView() { return null; const minutesSinceStart = - (eventStartHour - workingHoursStart) * 60 + + eventStartHour * 60 + eventStartMinute; const pixelsPerMinute = getSlotHeight(cellDuration) / cellDuration; @@ -6907,13 +6978,24 @@ export default function WeeklyView() { })} - {/* Right Navigation Arrows (after grid so it paints on top) */} -
+ {/* Right Navigation Arrows (now aligned with headers) */} +
} @@ -8105,6 +8187,8 @@ export default function WeeklyView() { onProjectsChanged={fetchProjects} kanbanStages={kanbanStages} saveKanbanStages={saveKanbanStages} + profile={profile} + setProfile={setProfile} /> ) } @@ -9282,6 +9366,8 @@ interface SettingsSidebarProps { quoteSourceUrls: string[]; quoteLanguages: string[]; }) => void; + profile: any; + setProfile: React.Dispatch>; setCurrentWeekStart: (d: Date) => void; quoteSourceUrls?: string[]; quoteLanguages?: string[]; @@ -9599,6 +9685,8 @@ function SettingsSidebar({ onProjectsChanged, kanbanStages, saveKanbanStages, + profile, + setProfile, }: SettingsSidebarProps) { const [activeTab, setActiveTab] = useState< "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation" @@ -9663,145 +9751,7 @@ function SettingsSidebar({ setTimeout(() => setConnMsg(null), 5000); }; - const [profile, setProfile] = useState<{ - name: string; - email: string; - timezone: string; - autoRolling?: boolean; - protectEventTimes?: boolean; - language?: string; - dateFormat?: string; - timeFormat?: string; - startHour?: number; - endHour?: number; - focusTimerDuration?: number; - focusBreakDuration?: number; - showTimeGrid?: boolean; - cellDuration?: number; - viewStyle?: string; - fontSize?: "S" | "M" | "L"; - showNextTask?: boolean; - showSomeday?: boolean; - showAllDayEvents?: boolean; - showSchedule?: boolean; - headlineFont?: string; - headlineFontSize?: string; - headlineFontWeight?: string; - dateFontFamily?: string; - dateFontSize?: string; - dateFontWeight?: string; - timeTaskFontFamily?: string; - timeTaskFontSize?: string; - timeTaskFontWeight?: string; - bodyFont?: string; - taskFontFamily?: string; - taskFontSize?: string; - taskFontWeight?: string; - eventFontFamily?: string; - eventFontSize?: string; - eventFontWeight?: string; - fontWeight?: string; - weekendColorSat?: string; - weekendColorSun?: string; - weekdayColor?: string; - dateColor?: string; - taskColor?: string; - todayHighlightColor?: string; - pastDayColor?: string; - goalFallbackType?: "quote" | "next_todo" | "default"; - quoteSourceUrl?: string; - goalDefaultSentence?: string; - goalFontFamily?: string; - goalFontSize?: string; - goalFontWeight?: string; - goalScope?: "week" | "day"; - dateLayout?: "above" | "below" | "left" | "right" | "hidden"; - mobileDateLayout?: "above" | "below" | "left" | "right" | "hidden"; - dateAlignment?: "left" | "center" | "right" | "tight"; - hourLabelFormat?: "short" | "full"; - showSubHourSlots?: boolean; - allDayPosition?: "above" | "below"; - cwFontFamily?: string; - cwFontSize?: string; - cwFontWeight?: string; - cwColor?: string; - yearFontFamily?: string; - yearFontSize?: string; - yearFontWeight?: string; - yearColor?: string; - dayHeaderGap?: string; - showTaskCheckboxes?: boolean; - quoteSourceUrls?: string[]; - quoteLanguages?: string[]; - startDayOffset?: number; - id?: string; - accountNumber?: number; - weekdayFormat?: "long" | "short" | "narrow" | "custom"; - weekdayCase?: "normal" | "capitalize" | "uppercase"; - customWeekdayNames?: string; - dateVerticalAlign?: "top" | "middle" | "bottom"; - headerDisplay?: "kw" | "month" | "month_year" | "date" | "custom"; - headerCustomFormat?: string; - }>({ - name: "", - email: "", - timezone: "Europe/Berlin", - autoRolling: false, - protectEventTimes: false, - language: "de", - dateFormat: "yyyy-MM-dd", - timeFormat: "24h", - startHour: 8, - endHour: 18, - focusTimerDuration: 25, - focusBreakDuration: 5, - headerDisplay: "kw", - headerCustomFormat: "KW WW | YYYY", - cellDuration: 30, - viewStyle: "list", - fontSize: "M", - showNextTask: false, - showSomeday: true, - showAllDayEvents: true, - showSchedule: true, - hourLabelFormat: "short", - dateVerticalAlign: "middle", - showSubHourSlots: true, - allDayPosition: "below", - goalFallbackType: "quote", - quoteSourceUrl: "", - headlineFont: "Inter", - headlineFontSize: "1.25rem", - headlineFontWeight: "900", - dateFontFamily: "Inter", - dateFontSize: "0.65rem", - dateFontWeight: "400", - timeTaskFontFamily: "Inter", - timeTaskFontSize: "0.75rem", - timeTaskFontWeight: "500", - bodyFont: "Inter", - taskFontFamily: "Inter", - taskFontSize: "0.9rem", - taskFontWeight: "400", - fontWeight: "400", - goalFontFamily: "Inter", - goalFontSize: "0.9rem", - goalFontWeight: "500", - goalScope: "week", - dateLayout: "right", - mobileDateLayout: "below", - dateAlignment: "center", - weekendColorSat: "#666666", - weekendColorSun: "#dc2626", - weekdayColor: "#888888", - dateColor: "#888888", - taskColor: "#333333", - todayHighlightColor: "#f0fafa", - pastDayColor: "#a6a6a7", - weekdayFormat: "long", - weekdayCase: "capitalize", - customWeekdayNames: "", - }); + // profile state removed (centralized in parent) const t = translations[profile.language || "en"] || translations["en"]; @@ -9809,14 +9759,10 @@ function SettingsSidebar({ // Font loading moved to top level WeeklyView component useEffect(() => { - try { - fetchProfile(); - // Trigger slide-in after mount - const timer = setTimeout(() => setIsVisible(true), 10); - return () => clearTimeout(timer); - } catch (e) { - console.error("Error mounting SettingsSidebar:", e); - } + setIsLoading(false); + // Trigger slide-in after mount + const timer = setTimeout(() => setIsVisible(true), 10); + return () => clearTimeout(timer); }, []); const handleClose = () => { @@ -9824,97 +9770,6 @@ function SettingsSidebar({ setTimeout(onClose, 300); }; - // Live preview: propagate styling changes immediately without Save - // Auto-save: debounce profile changes to the database - const profileLoadedRef = useRef(false); - const autoSaveTimerRef = useRef(null); - - useEffect(() => { - if (!profileLoadedRef.current) return; // Skip initial load from API - // Debounce: save after 800ms of no changes - if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); - autoSaveTimerRef.current = setTimeout(async () => { - try { - await fetch("/api/user/profile", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(profile), - }); - console.log("[SETTINGS] Auto-saved profile"); - } catch (err) { - console.error("[SETTINGS] Auto-save failed:", err); - } - }, 800); - return () => { - if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); - }; - }, [profile]); - useEffect(() => { - if (!profileLoadedRef.current) return; - if (!onSettingsChanged) return; - onSettingsChanged({ - showTimeGrid: showTimeGrid, - cellDuration: cellDuration, - viewStyle: viewStyle, - language: profile.language || "de", - dateFormat: profile.dateFormat || "MM/dd/yyyy", - timeFormat: profile.timeFormat || "12h", - startHour: profile.startHour || 8, - endHour: profile.endHour || 18, - fontSize: fontSize, - showNextTask: showNextTask, - showSomeday: showSomeday, - showAllDayEvents: showAllDay, - showSchedule: showSchedule, - headlineFont: profile.headlineFont || headlineFont, - headlineFontSize: profile.headlineFontSize || headlineFontSize, - headlineFontWeight: profile.headlineFontWeight || headlineFontWeight, - dateFontFamily: profile.dateFontFamily || dateFontFamily, - dateFontSize: profile.dateFontSize || dateFontSize, - dateFontWeight: profile.dateFontWeight || dateFontWeight, - timeTaskFontFamily: profile.timeTaskFontFamily || timeTaskFontFamily, - timeTaskFontSize: profile.timeTaskFontSize || timeTaskFontSize, - timeTaskFontWeight: profile.timeTaskFontWeight || timeTaskFontWeight, - bodyFont: profile.bodyFont || bodyFont, - taskFontFamily: profile.taskFontFamily || taskFontFamily, - taskFontSize: profile.taskFontSize || taskFontSize, - taskFontWeight: profile.taskFontWeight || taskFontWeight, - fontWeight: profile.fontWeight || fontWeight, - weekendColorSat: profile.weekendColorSat || weekendColorSat, - weekendColorSun: profile.weekendColorSun || weekendColorSun, - weekdayColor: profile.weekdayColor, - dateColor: profile.dateColor, - taskColor: profile.taskColor, - todayHighlightColor: profile.todayHighlightColor, - eventFontFamily: profile.eventFontFamily, - eventFontSize: profile.eventFontSize, - eventFontWeight: profile.eventFontWeight, - autoRolling: profile.autoRolling, - protectEventTimes: profile.protectEventTimes || protectEventTimes, - pastDayColor: profile.pastDayColor, - goalScope: profile.goalScope, - dateLayout: profile.dateLayout, - mobileDateLayout: profile.mobileDateLayout, - dateAlignment: profile.dateAlignment, - goalFontFamily: profile.goalFontFamily, - goalFontSize: profile.goalFontSize, - goalFontWeight: profile.goalFontWeight, - cwColor: profile.cwColor, - cwFontFamily: profile.cwFontFamily, - cwFontSize: profile.cwFontSize, - cwFontWeight: profile.cwFontWeight, - yearColor: profile.yearColor, - yearFontFamily: profile.yearFontFamily, - yearFontSize: profile.yearFontSize, - yearFontWeight: profile.yearFontWeight, - dayHeaderGap: profile.dayHeaderGap, - showTaskCheckboxes: profile.showTaskCheckboxes, - startDayOffset: profile.startDayOffset, - weekdayFormat: profile.weekdayFormat, - weekdayCase: profile.weekdayCase, - customWeekdayNames: profile.customWeekdayNames, - } as any); - }, [profile, showTimeGrid, cellDuration, viewStyle, fontSize, showNextTask, showSomeday, showAllDay, showSchedule]); const handleUpdateConnections = async (updatedConnections: any[]) => { onUpdateConnections(updatedConnections); @@ -9924,110 +9779,6 @@ function SettingsSidebar({ await onRemoveConnection(connectionId); }; - async function fetchProfile() { - try { - const res = await fetch("/api/user/profile"); - if (res.ok) { - const data = await res.json(); - if (data.user) { - setProfile({ - id: data.user.id || "", - name: data.user.name || "", - email: data.user.email || "", - timezone: data.user.timezone || "Europe/Berlin", - autoRolling: data.user.autoRolling || false, - protectEventTimes: data.user.protectEventTimes || false, - language: data.user.language || "de", - dateFormat: data.user.dateFormat || "yyyy-MM-dd", - timeFormat: data.user.timeFormat || "24h", - startHour: - data.user.startHour !== undefined ? data.user.startHour : 8, - endHour: data.user.endHour !== undefined ? data.user.endHour : 18, - focusTimerDuration: data.user.focusTimerDuration || 25, - showTimeGrid: - data.user.showTimeGrid !== undefined - ? data.user.showTimeGrid - : true, - cellDuration: data.user.cellDuration || 30, - weekdayFormat: data.user.weekdayFormat || "long", - weekdayCase: data.user.weekdayCase || "capitalize", - customWeekdayNames: data.user.customWeekdayNames || "", - viewStyle: data.user.viewStyle || "list", - fontSize: data.user.fontSize || "M", - headlineFont: data.user.headlineFont || "Inter", - bodyFont: data.user.bodyFont || "Inter", - fontWeight: data.user.fontWeight || "400", - showSchedule: - data.user.showSchedule !== undefined - ? data.user.showSchedule - : true, - focusBreakDuration: data.user.focusBreakDuration || 5, - headlineFontSize: data.user.headlineFontSize || "1.25rem", - headlineFontWeight: data.user.headlineFontWeight || "900", - goalFontWeight: data.user.goalFontWeight || "500", - dateFontFamily: data.user.dateFontFamily || "Inter", - dateFontSize: data.user.dateFontSize || "0.65rem", - dateFontWeight: data.user.dateFontWeight || "400", - timeTaskFontFamily: data.user.timeTaskFontFamily || "Inter", - timeTaskFontSize: data.user.timeTaskFontSize || "0.75rem", - timeTaskFontWeight: data.user.timeTaskFontWeight || "500", - taskFontFamily: data.user.taskFontFamily || "Inter", - taskFontSize: data.user.taskFontSize || "0.9rem", - taskFontWeight: data.user.taskFontWeight || "400", - eventFontFamily: data.user.eventFontFamily || "Inter", - eventFontSize: data.user.eventFontSize || "0.85rem", - eventFontWeight: data.user.eventFontWeight || "400", - goalFontFamily: data.user.goalFontFamily || "Inter", - goalFontSize: data.user.goalFontSize || "0.9rem", - goalScope: data.user.goalScope || "week", - dateLayout: data.user.dateLayout || "right", - mobileDateLayout: data.user.mobileDateLayout || "below", - dateAlignment: data.user.dateAlignment || "center", - weekendColorSat: data.user.weekendColorSat || "#666666", - weekendColorSun: data.user.weekendColorSun || "#dc2626", - weekdayColor: data.user.weekdayColor || "#888888", - dateColor: data.user.dateColor || "#888888", - taskColor: data.user.taskColor || "#333333", - todayHighlightColor: data.user.todayHighlightColor || "#f0fafa", - hourLabelFormat: data.user.hourLabelFormat || "short", - showSubHourSlots: data.user.showSubHourSlots !== undefined ? data.user.showSubHourSlots : true, - allDayPosition: data.user.allDayPosition || "below", - showTaskCheckboxes: data.user.showTaskCheckboxes || false, - startDayOffset: data.user.startDayOffset !== undefined ? data.user.startDayOffset : 0, - }); - - if (data.user.showTimeGrid !== undefined) - setShowTimeGrid(data.user.showTimeGrid); - if (data.user.cellDuration) - setCellDuration(data.user.cellDuration as CellDuration); - if (data.user.viewStyle) - setViewStyle(data.user.viewStyle as ViewStyle); - if (data.user.fontSize) - setFontSize(data.user.fontSize as "S" | "M" | "L"); - if (data.user.focusTimerDuration) - setFocusTimerDuration(data.user.focusTimerDuration); - if (data.user.showSchedule !== undefined) - setShowSchedule(data.user.showSchedule); - if (data.user.focusBreakDuration) - setFocusBreakDuration(data.user.focusBreakDuration); - - // Apply the initial start date offset if set - if (data.user.startDayOffset !== undefined && data.user.startDayOffset !== 0) { - const d = new Date(); - d.setHours(0, 0, 0, 0); - d.setDate(d.getDate() + data.user.startDayOffset); - setCurrentWeekStart(d); - } - } - } - } catch (e) { - console.error(e); - } finally { - setIsLoading(false); - // Enable live preview after initial load - setTimeout(() => { profileLoadedRef.current = true; }, 100); - } - } const handleGoogleConnect = () => { window.location.href = "/api/calendar/google/start"; @@ -10692,7 +10443,7 @@ function SettingsSidebar({ max="23" value={profile.startHour} onChange={(e) => - setProfile((prev) => ({ + setProfile((prev: any) => ({ ...prev, startHour: parseInt(e.target.value) || 0, })) @@ -10723,7 +10474,7 @@ function SettingsSidebar({ max="24" value={profile.endHour} onChange={(e) => - setProfile((prev) => ({ + setProfile((prev: any) => ({ ...prev, endHour: parseInt(e.target.value) || 0, })) @@ -10878,6 +10629,7 @@ function SettingsSidebar({ +
{profile.headerDisplay === "custom" && ( @@ -11277,7 +11029,7 @@ function SettingsSidebar({ - setProfile((p) => ({ + setProfile((p: any) => ({ ...p, goalFallbackType: e.target.value as any, })) @@ -13410,7 +13162,7 @@ function SettingsSidebar({ onChange={(e) => { const newUrls = [...(profile.quoteSourceUrls || [profile.quoteSourceUrl || ""])]; newUrls[idx] = e.target.value; - setProfile((p) => ({ ...p, quoteSourceUrls: newUrls })); + setProfile((p: any) => ({ ...p, quoteSourceUrls: newUrls })); }} className="weekly-input" placeholder="https://..." @@ -13427,7 +13179,7 @@ function SettingsSidebar({ type="button" onClick={() => { const newUrls = (profile.quoteSourceUrls || [profile.quoteSourceUrl || ""]).filter((_val: string, i: number) => i !== idx); - setProfile((p) => ({ ...p, quoteSourceUrls: newUrls })); + setProfile((p: any) => ({ ...p, quoteSourceUrls: newUrls })); }} style={{ padding: "8px", @@ -13446,7 +13198,7 @@ function SettingsSidebar({ type="button" onClick={() => { const newUrls = [...(profile.quoteSourceUrls || [profile.quoteSourceUrl || ""]), ""]; - setProfile((p) => ({ ...p, quoteSourceUrls: newUrls })); + setProfile((p: any) => ({ ...p, quoteSourceUrls: newUrls })); }} style={{ alignSelf: "flex-start", @@ -13518,7 +13270,7 @@ function SettingsSidebar({ ? current.filter((c: string) => c !== lang.code) : [...current, lang.code]; if (updated.length > 0) { - setProfile((p) => ({ ...p, quoteLanguages: updated })); + setProfile((p: any) => ({ ...p, quoteLanguages: updated })); } }} style={{ display: "none" }} @@ -13550,7 +13302,7 @@ function SettingsSidebar({ type="text" value={profile.goalDefaultSentence || ""} onChange={(e) => - setProfile((p) => ({ + setProfile((p: any) => ({ ...p, goalDefaultSentence: e.target.value, })) @@ -14496,7 +14248,7 @@ function SettingsSidebar({
)}
-
+
); }