fix: keep display settings per device

This commit is contained in:
mARTin-B78 2026-09-05 08:00:38 +02:00
parent 8c82db037a
commit 3ed7ec9825
6 changed files with 71 additions and 34 deletions

View File

@ -2,6 +2,15 @@
All notable changes to this project are documented in this file. 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 ## [1.116.0] - 2026-09-04
### Added ### Added

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.116.0", "version": "1.116.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.116.0", "version": "1.116.1",
"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.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", "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

@ -17,7 +17,12 @@ export async function GET(request: NextRequest) {
if (input.length < 2) { if (input.length < 2) {
return NextResponse.json({ suggestions: [] }); 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 { try {
const params = new URLSearchParams({ const params = new URLSearchParams({
q: input, q: input,
@ -29,6 +34,7 @@ export async function GET(request: NextRequest) {
'User-Agent': 'My-Weekly-ToDo-List/1.0 (location autocomplete)', 'User-Agent': 'My-Weekly-ToDo-List/1.0 (location autocomplete)',
}, },
cache: 'no-store', cache: 'no-store',
signal: controller.signal,
}); });
if (!response.ok) { if (!response.ok) {
@ -55,5 +61,7 @@ export async function GET(request: NextRequest) {
} catch (error) { } catch (error) {
console.error('[PLACES] Autocomplete request failed:', error); console.error('[PLACES] Autocomplete request failed:', error);
return NextResponse.json({ suggestions: [] }); return NextResponse.json({ suggestions: [] });
} finally {
clearTimeout(timeout);
} }
} }

View File

@ -202,14 +202,21 @@ export default function CalendarEventModal({
const dialogRef = useRef<HTMLDivElement>(null); const dialogRef = useRef<HTMLDivElement>(null);
// Location autocomplete // Location autocomplete
const [locationFocused, setLocationFocused] = useState(false);
const [locationSuggestions, setLocationSuggestions] = useState<LocationSuggestion[]>([]); const [locationSuggestions, setLocationSuggestions] = useState<LocationSuggestion[]>([]);
const locationRef = useRef<HTMLDivElement>(null); const locationRef = useRef<HTMLDivElement>(null);
const locationRequestRef = useRef(0); const locationRequestRef = useRef(0);
const locationSearchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const locationSearchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const locationAbortRef = useRef<AbortController | null>(null);
const dismissLocationSuggestions = useCallback(() => {
++locationRequestRef.current;
locationAbortRef.current?.abort();
if (locationSearchTimerRef.current) clearTimeout(locationSearchTimerRef.current);
setLocationSuggestions([]);
}, []);
const updateLocationSuggestions = useCallback((val: string) => { const updateLocationSuggestions = useCallback((val: string) => {
const requestId = ++locationRequestRef.current; const requestId = ++locationRequestRef.current;
locationAbortRef.current?.abort();
if (locationSearchTimerRef.current) clearTimeout(locationSearchTimerRef.current); if (locationSearchTimerRef.current) clearTimeout(locationSearchTimerRef.current);
const trimmed = val.trim(); const trimmed = val.trim();
@ -229,9 +236,12 @@ export default function CalendarEventModal({
input: trimmed, input: trimmed,
languageCode: language.slice(0, 10), 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; if (!response.ok || requestId !== locationRequestRef.current) return;
const data = await response.json(); const data = await response.json();
if (requestId !== locationRequestRef.current) return;
const mapMatches: LocationSuggestion[] = (data.suggestions || []) const mapMatches: LocationSuggestion[] = (data.suggestions || [])
.filter((suggestion: any) => suggestion.label) .filter((suggestion: any) => suggestion.label)
.map((suggestion: any) => ({ ...suggestion, source: 'openstreetmap' })); .map((suggestion: any) => ({ ...suggestion, source: 'openstreetmap' }));
@ -241,7 +251,7 @@ export default function CalendarEventModal({
...mapMatches.filter(suggestion => !localLabels.has(suggestion.label.toLowerCase())), ...mapMatches.filter(suggestion => !localLabels.has(suggestion.label.toLowerCase())),
].slice(0, 7)); ].slice(0, 7));
} catch { } catch {
// Saved-location suggestions remain available if Google is unavailable. // Saved locations remain available if Photon is unavailable.
} }
}, 250); }, 250);
}, [language, savedLocations]); }, [language, savedLocations]);
@ -271,12 +281,14 @@ export default function CalendarEventModal({
useEffect(() => { useEffect(() => {
const handler = (e: MouseEvent) => { const handler = (e: MouseEvent) => {
if (locationRef.current && !locationRef.current.contains(e.target as Node)) { if (locationRef.current && !locationRef.current.contains(e.target as Node)) {
setLocationSuggestions([]); dismissLocationSuggestions();
} }
}; };
document.addEventListener('mousedown', handler); document.addEventListener('mousedown', handler);
return () => { return () => {
document.removeEventListener('mousedown', handler); document.removeEventListener('mousedown', handler);
++locationRequestRef.current;
locationAbortRef.current?.abort();
if (locationSearchTimerRef.current) clearTimeout(locationSearchTimerRef.current); if (locationSearchTimerRef.current) clearTimeout(locationSearchTimerRef.current);
}; };
}, []); }, []);
@ -831,9 +843,9 @@ export default function CalendarEventModal({
type="text" type="text"
value={location} value={location}
onChange={e => { setLocation(e.target.value); updateLocationSuggestions(e.target.value); }} onChange={e => { setLocation(e.target.value); updateLocationSuggestions(e.target.value); }}
onFocus={() => { setLocationFocused(true); updateLocationSuggestions(location); }} onFocus={() => updateLocationSuggestions(location)}
onBlur={() => setTimeout(() => setLocationSuggestions([]), 150)} onBlur={dismissLocationSuggestions}
onKeyDown={e => { if (e.key === 'Escape') setLocationSuggestions([]); }} onKeyDown={e => { if (e.key === 'Escape') { e.stopPropagation(); dismissLocationSuggestions(); } }}
placeholder={language === 'de' ? 'Ort hinzufügen' : 'Add location'} placeholder={language === 'de' ? 'Ort hinzufügen' : 'Add location'}
aria-label={language === 'de' ? 'Ort' : 'Location'} aria-label={language === 'de' ? 'Ort' : 'Location'}
aria-autocomplete="list" aria-autocomplete="list"
@ -861,7 +873,7 @@ export default function CalendarEventModal({
key={`${suggestion.source}-${suggestion.placeId || suggestion.label}-${i}`} key={`${suggestion.source}-${suggestion.placeId || suggestion.label}-${i}`}
role="option" role="option"
aria-selected={false} aria-selected={false}
onMouseDown={e => { e.preventDefault(); setLocation(suggestion.label); setLocationSuggestions([]); }} onMouseDown={e => { e.preventDefault(); setLocation(suggestion.label); dismissLocationSuggestions(); }}
style={{ style={{
padding: '6px 10px', fontSize: '0.78rem', padding: '6px 10px', fontSize: '0.78rem',
cursor: 'pointer', color: 'var(--weekly-text)', cursor: 'pointer', color: 'var(--weekly-text)',

View File

@ -143,6 +143,17 @@ function setCookie(name: string, value: string, days: number = 365) {
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`; 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>> { function readDeviceViewCookie(): Record<string, Record<string, any>> {
const raw = getCookie("device_view_settings"); const raw = getCookie("device_view_settings");
if (!raw) return {}; if (!raw) return {};
@ -612,17 +623,8 @@ export default function WeeklyView() {
// Responsive: auto-adjust viewDays based on screen orientation / width // Responsive: auto-adjust viewDays based on screen orientation / width
useEffect(() => { 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 handleResize = () => {
const responsiveDays = getResponsiveViewDays(window.innerWidth, window.innerHeight); const responsiveDays = resolveViewDays(savedViewDaysRef.current);
setViewDays(responsiveDays); setViewDays(responsiveDays);
}; };
@ -1439,10 +1441,7 @@ export default function WeeklyView() {
if (profileData.viewStyle) setViewStyle(profileData.viewStyle); if (profileData.viewStyle) setViewStyle(profileData.viewStyle);
if (profileData.viewDays) { if (profileData.viewDays) {
savedViewDaysRef.current = profileData.viewDays; savedViewDaysRef.current = profileData.viewDays;
const w = window.innerWidth, h = window.innerHeight; setViewDays(resolveViewDays(profileData.viewDays));
if (w <= 768) setViewDays(h > w ? 1 : 3);
else if (w <= 1024) setViewDays(Math.min(profileData.viewDays, 5));
else setViewDays(profileData.viewDays);
} }
if (profileData.showTimeGrid !== undefined) setShowTimeGrid(profileData.showTimeGrid); if (profileData.showTimeGrid !== undefined) setShowTimeGrid(profileData.showTimeGrid);
if (profileData.showSomeday !== undefined) setShowSomeday(profileData.showSomeday); if (profileData.showSomeday !== undefined) setShowSomeday(profileData.showSomeday);
@ -1529,10 +1528,25 @@ export default function WeeklyView() {
if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
autoSaveTimerRef.current = setTimeout(async () => { autoSaveTimerRef.current = setTimeout(async () => {
try { 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", { await fetch("/api/user/profile", {
method: "PATCH", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(profile), body: JSON.stringify(cloudProfile),
}); });
console.log("[SETTINGS] Auto-saved profile"); console.log("[SETTINGS] Auto-saved profile");
} catch (err) { } catch (err) {
@ -2549,10 +2563,7 @@ export default function WeeklyView() {
} }
if (data.user.viewDays !== undefined) { if (data.user.viewDays !== undefined) {
savedViewDaysRef.current = data.user.viewDays; savedViewDaysRef.current = data.user.viewDays;
const w = window.innerWidth, h = window.innerHeight; setViewDays(resolveViewDays(data.user.viewDays));
if (w <= 768) setViewDays(h > w ? 1 : 3);
else if (w <= 1024) setViewDays(Math.min(data.user.viewDays, 5));
else setViewDays(data.user.viewDays);
} }
if (data.user.cellDuration !== undefined) if (data.user.cellDuration !== undefined)
setCellDuration(data.user.cellDuration as CellDuration); setCellDuration(data.user.cellDuration as CellDuration);
@ -2562,10 +2573,7 @@ export default function WeeklyView() {
if (cookieViewDays) { if (cookieViewDays) {
const v = Number(cookieViewDays); const v = Number(cookieViewDays);
savedViewDaysRef.current = v; savedViewDaysRef.current = v;
const w = window.innerWidth, h = window.innerHeight; setViewDays(resolveViewDays(data.user.viewDays));
if (w <= 768) setViewDays(h > w ? 1 : 3);
else if (w <= 1024) setViewDays(Math.min(v, 5));
else setViewDays(v);
} }
if (data.user.weekdayFormat) { if (data.user.weekdayFormat) {
setProfile((prev: any) => ({ ...prev, weekdayFormat: data.user.weekdayFormat })); setProfile((prev: any) => ({ ...prev, weekdayFormat: data.user.weekdayFormat }));