fix: keep display settings per device
This commit is contained in:
parent
8c82db037a
commit
3ed7ec9825
@ -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
4
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)',
|
||||||
|
|||||||
@ -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 }));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user