From 4757dc3cdb5668c4ee6a927d1b7685fba5ece600 Mon Sep 17 00:00:00 2001 From: mARTin Date: Wed, 18 Mar 2026 01:09:36 +0100 Subject: [PATCH] feat: per-view settings for hour format, sub-hours, weather, checkboxes Add viewSettings JSON field to User model storing per-view overrides. Settings like hour label format, sub-hour labels, weather, and task checkboxes can now be set per view (simple/calendar/list/kanban) or globally. A clickable badge next to each setting shows "All" (global) or the current view name (per-view). Click to toggle scope. Also fixes hourLabelFormat not actually being applied to time column rendering (was hardcoded, now uses effective per-view value). v1.49.0 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- prisma/schema.prisma | 1 + src/app/api/user/profile/route.ts | 5 +- src/components/WeeklyView.tsx | 164 ++++++++++++++++++++++++++---- 4 files changed, 152 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index f3cbb24..66c089e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.48.3", + "version": "1.49.0", "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 4eb269d..08944ef 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -105,6 +105,7 @@ model User { quoteSourceUrls String[] @default([]) quoteLanguages String[] @default(["en", "de"]) kanbanStages String? + viewSettings Json? accounts Account[] cachedCalendarEvents CachedCalendarEvent[] calendarConnections CalendarConnection[] diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts index 2db5aa7..a0ead7e 100644 --- a/src/app/api/user/profile/route.ts +++ b/src/app/api/user/profile/route.ts @@ -101,6 +101,7 @@ export async function GET(request: NextRequest) { weatherLat: true, weatherLon: true, weatherLocation: true, + viewSettings: true, createdAt: true } }); @@ -144,7 +145,7 @@ export async function PATCH(request: NextRequest) { showTaskCheckboxes, dayHeaderGap, showCompletedTasks, showLines, startDayOffset, weekdayFormat, weekdayCase, customWeekdayNames, quoteSourceUrls, quoteLanguages, kanbanStages, lightTheme, darkTheme, notificationsEnabled, mobileFontScale, - weatherEnabled, weatherLat, weatherLon, weatherLocation + weatherEnabled, weatherLat, weatherLon, weatherLocation, viewSettings } = body; const updateData: any = { @@ -234,6 +235,7 @@ export async function PATCH(request: NextRequest) { ...(weatherLat !== undefined && { weatherLat: weatherLat !== null ? parseFloat(weatherLat) : null }), ...(weatherLon !== undefined && { weatherLon: weatherLon !== null ? parseFloat(weatherLon) : null }), ...(weatherLocation !== undefined && { weatherLocation }), + ...(viewSettings !== undefined && { viewSettings }), }; if (password && password.trim() !== "") { updateData.passwordHash = await bcrypt.hash(password, 10); @@ -332,6 +334,7 @@ export async function PATCH(request: NextRequest) { weatherLat: true, weatherLon: true, weatherLocation: true, + viewSettings: true, } }); diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 4a78262..1da3015 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -1950,6 +1950,72 @@ export default function WeeklyView() { slotIdx?: number; } | null>(null); const [viewStyle, setViewStyle] = useState("simple"); + + // Per-view settings: overrides that apply only to a specific view + type PerViewOverrides = { hourLabelFormat?: "short" | "full"; showSubHourSlots?: boolean; weatherEnabled?: boolean; showTaskCheckboxes?: boolean }; + const PER_VIEW_KEYS = ["hourLabelFormat", "showSubHourSlots", "weatherEnabled", "showTaskCheckboxes"] as const; + const [viewSettings, setViewSettings] = useState>({}); + + const getEffective = (key: K, globalVal: PerViewOverrides[K]): PerViewOverrides[K] => { + const vs = viewSettings[viewStyle]; + if (vs && vs[key] !== undefined) return vs[key] as PerViewOverrides[K]; + return globalVal; + }; + const isPerView = (key: keyof PerViewOverrides): boolean => { + const vs = viewSettings[viewStyle]; + return !!(vs && vs[key] !== undefined); + }; + const saveViewSetting = async (key: K, value: PerViewOverrides[K], perView: boolean) => { + const updated = { ...viewSettings }; + if (perView) { + updated[viewStyle] = { ...(updated[viewStyle] || {}), [key]: value }; + } else { + // Remove per-view overrides for this key from ALL views and set globally + for (const v of Object.keys(updated)) { + if (updated[v] && updated[v][key] !== undefined) { + const { [key]: _, ...rest } = updated[v] as any; + updated[v] = rest; + } + } + } + setViewSettings(updated); + // Save to DB + try { + await fetch("/api/user/profile", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ viewSettings: updated }), + }); + } catch (e) { console.error("Failed to save view settings:", e); } + }; + const togglePerView = async (key: keyof PerViewOverrides, globalVal: any) => { + if (isPerView(key)) { + // Remove per-view override (revert to global) + const updated = { ...viewSettings }; + if (updated[viewStyle]) { + const { [key]: _, ...rest } = updated[viewStyle] as any; + updated[viewStyle] = rest; + } + setViewSettings(updated); + try { + await fetch("/api/user/profile", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ viewSettings: updated }), + }); + } catch (e) { console.error("Failed to save view settings:", e); } + } else { + // Set per-view override to current global value + saveViewSetting(key, globalVal, true); + } + }; + + // Effective per-view values (override if set for current view, else global) + const effectiveHourLabelFormat = getEffective("hourLabelFormat", hourLabelFormat); + const effectiveShowSubHourSlots = getEffective("showSubHourSlots", showSubHourSlots); + const effectiveWeatherEnabled = getEffective("weatherEnabled", profile.weatherEnabled); + const effectiveShowTaskCheckboxes = getEffective("showTaskCheckboxes", profile.showTaskCheckboxes); + const defaultKanbanStages: KanbanStage[] = [ { id: "backlog", name: "Backlog", color: "#94a3b8" }, { id: "todo", name: "To Do", color: "#3b82f6" }, @@ -2132,6 +2198,7 @@ export default function WeeklyView() { if (profileData.hourLabelFormat) setHourLabelFormat(profileData.hourLabelFormat); if (profileData.showSubHourSlots !== undefined) setShowSubHourSlots(profileData.showSubHourSlots); if (profileData.allDayPosition) setAllDayPosition(profileData.allDayPosition); + if (profileData.viewSettings) setViewSettings(profileData.viewSettings); } } } catch (err) { @@ -3010,6 +3077,7 @@ export default function WeeklyView() { setShowSubHourSlots(data.user.showSubHourSlots); if (data.user.allDayPosition) setAllDayPosition(data.user.allDayPosition as "above" | "below"); + if (data.user.viewSettings) setViewSettings(data.user.viewSettings); if (data.user.headlineFont) setHeadlineFont(data.user.headlineFont); if (data.user.headlineFontSize) setHeadlineFontSize(data.user.headlineFontSize); @@ -6599,7 +6667,7 @@ export default function WeeklyView() { const hour = getHourFromSlot(slot); const minutes = slot.split(":")[1]; const isHourStart = minutes === "00"; - if (!isHourStart && !showSubHourSlots) return ( + if (!isHourStart && !effectiveShowSubHourSlots) return (
jumpToHour(hour) : undefined} title={isHourStart ? `Jump to ${hour}:00` : undefined} > - {(isHourStart || showSubHourSlots) && ( - {formatHour(hour, parseInt(minutes), (isHourStart ? 'short' : 'full') as "short" | "full", timeFormat)} + {(isHourStart || effectiveShowSubHourSlots) && ( + {formatHour(hour, parseInt(minutes), (isHourStart ? effectiveHourLabelFormat : 'full') as "short" | "full", timeFormat)} )}
); @@ -6868,7 +6936,7 @@ export default function WeeklyView() { deleteSubTask={deleteSubTask} onSetEditingTaskId={setEditingTaskId} workingHoursStart={workingHoursStart} - showTaskCheckboxes={profile.showTaskCheckboxes} + showTaskCheckboxes={effectiveShowTaskCheckboxes} projects={projects} onProjectAssign={assignProject} kanbanStages={kanbanStages} @@ -6945,7 +7013,7 @@ export default function WeeklyView() { onDrop={handleSlotDrop} > {/* Weather indicator for hour-start slots */} - {isHourStart && profile.weatherEnabled && (() => { + {isHourStart && effectiveWeatherEnabled && (() => { const h = parseInt(slot.split(":")[0]); const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}T${String(h).padStart(2, "0")}:00`; const w = weatherData[dateStr]; @@ -7167,7 +7235,7 @@ export default function WeeklyView() { onUpdateSubTask={updateSubTask} editingTaskId={editingTaskId} onSetEditingTaskId={setEditingTaskId} - showTaskCheckboxes={profile.showTaskCheckboxes} + showTaskCheckboxes={effectiveShowTaskCheckboxes} projects={projects} onProjectAssign={assignProject} kanbanStages={kanbanStages} @@ -7863,7 +7931,7 @@ export default function WeeklyView() { onUpdateSubTask={updateSubTask} editingTaskId={editingTaskId} onSetEditingTaskId={setEditingTaskId} - showTaskCheckboxes={profile.showTaskCheckboxes} + showTaskCheckboxes={effectiveShowTaskCheckboxes} projects={projects} onProjectAssign={assignProject} kanbanStages={kanbanStages} @@ -7934,7 +8002,7 @@ export default function WeeklyView() { onUpdateSubTask={updateSubTask} editingTaskId={editingTaskId} onSetEditingTaskId={setEditingTaskId} - showTaskCheckboxes={profile.showTaskCheckboxes} + showTaskCheckboxes={effectiveShowTaskCheckboxes} projects={projects} onProjectAssign={assignProject} kanbanStages={kanbanStages} @@ -8443,6 +8511,12 @@ export default function WeeklyView() { onStartHourChange: (h: number) => { setStartHour(h); saveSetting("startHour", h); }, onEndHourChange: (h: number) => { setEndHour(h); saveSetting("endHour", h); }, }} + perView={{ + isPerView: isPerView as any, + togglePerView: togglePerView as any, + saveViewSetting: saveViewSetting as any, + viewLabel: viewStyle === "simple" ? (language === "de" ? "Einfach" : "Simple") : viewStyle === "calendar" ? (language === "de" ? "Kalender" : "Calendar") : viewStyle === "list" ? (language === "de" ? "Liste" : "List") : "Kanban", + }} /> ) } @@ -9978,6 +10052,12 @@ interface SettingsSidebarProps { onStartHourChange: (h: number) => void; onEndHourChange: (h: number) => void; }; + perView: { + isPerView: (key: string) => boolean; + togglePerView: (key: string, globalVal: any) => void; + saveViewSetting: (key: string, value: any, perView: boolean) => void; + viewLabel: string; + }; } // Notes Sidebar Component interface NotesSidebarProps { @@ -10202,7 +10282,33 @@ function SettingsSidebar({ setProfile, isMobile: isMobileSidebar, mobileActions, + perView, }: SettingsSidebarProps) { + // Per-view badge: shows which view a setting applies to, click to toggle + const PerViewBadge = ({ settingKey, globalVal }: { settingKey: string; globalVal: any }) => { + const isPV = perView.isPerView(settingKey); + return ( + + ); + }; + const [activeTab, setActiveTab] = useState< "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation" >(initialTab || "general"); @@ -10850,16 +10956,20 @@ function SettingsSidebar({ type="checkbox" id="showTaskCheckboxes" checked={profile.showTaskCheckboxes || false} - onChange={(e) => - setProfile({ ...profile, showTaskCheckboxes: e.target.checked }) - } + onChange={(e) => { + setProfile({ ...profile, showTaskCheckboxes: e.target.checked }); + if (perView.isPerView("showTaskCheckboxes")) { + perView.saveViewSetting("showTaskCheckboxes", e.target.checked, true); + } + }} style={{ width: "16px", height: "16px" }} />