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.
|
||||
|
||||
## [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
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,14 +202,21 @@ export default function CalendarEventModal({
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Location autocomplete
|
||||
const [locationFocused, setLocationFocused] = useState(false);
|
||||
const [locationSuggestions, setLocationSuggestions] = useState<LocationSuggestion[]>([]);
|
||||
const locationRef = useRef<HTMLDivElement>(null);
|
||||
const locationRequestRef = useRef(0);
|
||||
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 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)',
|
||||
|
||||
@ -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<string, Record<string, any>> {
|
||||
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 }));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user