From 3ed7ec98259f2f7b154dda868f4b29de2f69a056 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Sat, 5 Sep 2026 08:00:38 +0200 Subject: [PATCH] fix: keep display settings per device --- CHANGELOG.md | 9 ++++ package-lock.json | 4 +- package.json | 2 +- src/app/api/places/autocomplete/route.ts | 8 ++++ src/components/CalendarEventModal.tsx | 28 ++++++++---- src/components/WeeklyView.tsx | 54 ++++++++++++++---------- 6 files changed, 71 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df619b5..1e69145 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project are documented in this file. +## [1.116.1] - 2026-09-05 + +### Fixed +- Display preferences such as the number of visible days now remain device-specific. + Automatic profile saving no longer sends browser-only settings back to the shared account. +- An explicitly selected day count now survives mobile reloads and orientation changes. +- Cancelled or superseded location autocomplete requests no longer update the event form. +- Photon autocomplete requests now have a five-second timeout and input length limit. + ## [1.116.0] - 2026-09-04 ### Added diff --git a/package-lock.json b/package-lock.json index cf63091..3a57e2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "my-weekly-todo-list", - "version": "1.116.0", + "version": "1.116.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "my-weekly-todo-list", - "version": "1.116.0", + "version": "1.116.1", "license": "MIT", "dependencies": { "@auth/prisma-adapter": "^2.11.1", diff --git a/package.json b/package.json index 55b4341..d1a1301 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.116.0", + "version": "1.116.1", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/app/api/places/autocomplete/route.ts b/src/app/api/places/autocomplete/route.ts index 2bad254..9f628ca 100644 --- a/src/app/api/places/autocomplete/route.ts +++ b/src/app/api/places/autocomplete/route.ts @@ -17,7 +17,12 @@ export async function GET(request: NextRequest) { if (input.length < 2) { return NextResponse.json({ suggestions: [] }); } + if (input.length > 200) { + return NextResponse.json({ suggestions: [], error: 'Search is too long' }, { status: 400 }); + } + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); try { const params = new URLSearchParams({ q: input, @@ -29,6 +34,7 @@ export async function GET(request: NextRequest) { 'User-Agent': 'My-Weekly-ToDo-List/1.0 (location autocomplete)', }, cache: 'no-store', + signal: controller.signal, }); if (!response.ok) { @@ -55,5 +61,7 @@ export async function GET(request: NextRequest) { } catch (error) { console.error('[PLACES] Autocomplete request failed:', error); return NextResponse.json({ suggestions: [] }); + } finally { + clearTimeout(timeout); } } diff --git a/src/components/CalendarEventModal.tsx b/src/components/CalendarEventModal.tsx index 535d20f..dba61e6 100644 --- a/src/components/CalendarEventModal.tsx +++ b/src/components/CalendarEventModal.tsx @@ -202,14 +202,21 @@ export default function CalendarEventModal({ const dialogRef = useRef(null); // Location autocomplete - const [locationFocused, setLocationFocused] = useState(false); const [locationSuggestions, setLocationSuggestions] = useState([]); const locationRef = useRef(null); const locationRequestRef = useRef(0); const locationSearchTimerRef = useRef | null>(null); + const locationAbortRef = useRef(null); + const dismissLocationSuggestions = useCallback(() => { + ++locationRequestRef.current; + locationAbortRef.current?.abort(); + if (locationSearchTimerRef.current) clearTimeout(locationSearchTimerRef.current); + setLocationSuggestions([]); + }, []); const updateLocationSuggestions = useCallback((val: string) => { const requestId = ++locationRequestRef.current; + locationAbortRef.current?.abort(); if (locationSearchTimerRef.current) clearTimeout(locationSearchTimerRef.current); const trimmed = val.trim(); @@ -229,9 +236,12 @@ export default function CalendarEventModal({ input: trimmed, languageCode: language.slice(0, 10), }); - const response = await fetch(`/api/places/autocomplete?${params}`, { cache: 'no-store' }); + const controller = new AbortController(); + locationAbortRef.current = controller; + const response = await fetch(`/api/places/autocomplete?${params}`, { cache: 'no-store', signal: controller.signal }); if (!response.ok || requestId !== locationRequestRef.current) return; const data = await response.json(); + if (requestId !== locationRequestRef.current) return; const mapMatches: LocationSuggestion[] = (data.suggestions || []) .filter((suggestion: any) => suggestion.label) .map((suggestion: any) => ({ ...suggestion, source: 'openstreetmap' })); @@ -241,7 +251,7 @@ export default function CalendarEventModal({ ...mapMatches.filter(suggestion => !localLabels.has(suggestion.label.toLowerCase())), ].slice(0, 7)); } catch { - // Saved-location suggestions remain available if Google is unavailable. + // Saved locations remain available if Photon is unavailable. } }, 250); }, [language, savedLocations]); @@ -271,12 +281,14 @@ export default function CalendarEventModal({ useEffect(() => { const handler = (e: MouseEvent) => { if (locationRef.current && !locationRef.current.contains(e.target as Node)) { - setLocationSuggestions([]); + dismissLocationSuggestions(); } }; document.addEventListener('mousedown', handler); return () => { document.removeEventListener('mousedown', handler); + ++locationRequestRef.current; + locationAbortRef.current?.abort(); if (locationSearchTimerRef.current) clearTimeout(locationSearchTimerRef.current); }; }, []); @@ -831,9 +843,9 @@ export default function CalendarEventModal({ type="text" value={location} onChange={e => { setLocation(e.target.value); updateLocationSuggestions(e.target.value); }} - onFocus={() => { setLocationFocused(true); updateLocationSuggestions(location); }} - onBlur={() => setTimeout(() => setLocationSuggestions([]), 150)} - onKeyDown={e => { if (e.key === 'Escape') setLocationSuggestions([]); }} + onFocus={() => updateLocationSuggestions(location)} + onBlur={dismissLocationSuggestions} + onKeyDown={e => { if (e.key === 'Escape') { e.stopPropagation(); dismissLocationSuggestions(); } }} placeholder={language === 'de' ? 'Ort hinzufügen' : 'Add location'} aria-label={language === 'de' ? 'Ort' : 'Location'} aria-autocomplete="list" @@ -861,7 +873,7 @@ export default function CalendarEventModal({ key={`${suggestion.source}-${suggestion.placeId || suggestion.label}-${i}`} role="option" aria-selected={false} - onMouseDown={e => { e.preventDefault(); setLocation(suggestion.label); setLocationSuggestions([]); }} + onMouseDown={e => { e.preventDefault(); setLocation(suggestion.label); dismissLocationSuggestions(); }} style={{ padding: '6px 10px', fontSize: '0.78rem', cursor: 'pointer', color: 'var(--weekly-text)', diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 619d750..53472dd 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -143,6 +143,17 @@ function setCookie(name: string, value: string, days: number = 365) { document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`; } +// Explicit browser choices take precedence over responsive defaults on every load. +function resolveViewDays(fallback: number): number { + const raw = getCookie("setting_viewDays"); + const saved = raw === null ? NaN : Number(raw); + if (Number.isInteger(saved) && saved >= 1 && saved <= 31) return saved; + const days = clampViewDays(fallback || 7); + if (typeof window === "undefined") return days; + if (window.innerWidth <= 768) return window.innerHeight > window.innerWidth ? 1 : 3; + return window.innerWidth <= 1024 ? Math.min(days, 5) : days; +} + function readDeviceViewCookie(): Record> { const raw = getCookie("device_view_settings"); if (!raw) return {}; @@ -612,17 +623,8 @@ export default function WeeklyView() { // Responsive: auto-adjust viewDays based on screen orientation / width useEffect(() => { - const getResponsiveViewDays = (width: number, height: number): number => { - if (width <= 768) { - // Mobile: portrait → 1 day, landscape → 3 days - return height > width ? 1 : 3; - } - if (width <= 1024) return Math.min(savedViewDaysRef.current, 5); - return savedViewDaysRef.current; - }; - const handleResize = () => { - const responsiveDays = getResponsiveViewDays(window.innerWidth, window.innerHeight); + const responsiveDays = resolveViewDays(savedViewDaysRef.current); setViewDays(responsiveDays); }; @@ -1439,10 +1441,7 @@ export default function WeeklyView() { if (profileData.viewStyle) setViewStyle(profileData.viewStyle); if (profileData.viewDays) { savedViewDaysRef.current = profileData.viewDays; - const w = window.innerWidth, h = window.innerHeight; - if (w <= 768) setViewDays(h > w ? 1 : 3); - else if (w <= 1024) setViewDays(Math.min(profileData.viewDays, 5)); - else setViewDays(profileData.viewDays); + setViewDays(resolveViewDays(profileData.viewDays)); } if (profileData.showTimeGrid !== undefined) setShowTimeGrid(profileData.showTimeGrid); if (profileData.showSomeday !== undefined) setShowSomeday(profileData.showSomeday); @@ -1529,10 +1528,25 @@ export default function WeeklyView() { if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); autoSaveTimerRef.current = setTimeout(async () => { try { + // These preferences belong to the browser/device. Do not send + // them through the full-profile auto-save, otherwise changing + // the day count on a phone overwrites the desktop preference. + const { + viewDays: _viewDays, + cellDuration: _cellDuration, + startHour: _startHour, + endHour: _endHour, + fontSize: _fontSize, + showSubHourSlots: _showSubHourSlots, + viewStyle: _viewStyle, + showTimeGrid: _showTimeGrid, + startDayOffset: _startDayOffset, + ...cloudProfile + } = profile as any; await fetch("/api/user/profile", { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(profile), + body: JSON.stringify(cloudProfile), }); console.log("[SETTINGS] Auto-saved profile"); } catch (err) { @@ -2549,10 +2563,7 @@ export default function WeeklyView() { } if (data.user.viewDays !== undefined) { savedViewDaysRef.current = data.user.viewDays; - const w = window.innerWidth, h = window.innerHeight; - if (w <= 768) setViewDays(h > w ? 1 : 3); - else if (w <= 1024) setViewDays(Math.min(data.user.viewDays, 5)); - else setViewDays(data.user.viewDays); + setViewDays(resolveViewDays(data.user.viewDays)); } if (data.user.cellDuration !== undefined) setCellDuration(data.user.cellDuration as CellDuration); @@ -2562,10 +2573,7 @@ export default function WeeklyView() { if (cookieViewDays) { const v = Number(cookieViewDays); savedViewDaysRef.current = v; - const w = window.innerWidth, h = window.innerHeight; - if (w <= 768) setViewDays(h > w ? 1 : 3); - else if (w <= 1024) setViewDays(Math.min(v, 5)); - else setViewDays(v); + setViewDays(resolveViewDays(data.user.viewDays)); } if (data.user.weekdayFormat) { setProfile((prev: any) => ({ ...prev, weekdayFormat: data.user.weekdayFormat }));