From 1b5e307eb621db012f4f7d85a179daec6a8566d7 Mon Sep 17 00:00:00 2001 From: mrramam Date: Sat, 29 Aug 2026 08:48:42 -0700 Subject: [PATCH 1/3] feat(calendar): add a today control and idle auto-return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A wall display left on last month looks current and is not. The period label doubles as a "today" button — tapping it returns to today, and it goes inert (reading "· Today") when already there. Desktop also gets a separate 📅 button; on a phone the header has no room for one, so the label carries it alone. It is a ButtonBase, so it is keyboard reachable. Auto-return brings the view back to today after a configurable idle period (default 20 minutes, 0 or empty disables), stored per device in calendarWidgetSettings.idleReturnMinutes. Any interaction restarts the countdown; the timer is cleared on unmount and does not run while the browser tab is hidden. Only the date is reset — a household that chose week view keeps it. Note the countdown restarts rather than resumes when a hidden browser tab becomes visible again. That is invisible on a kiosk but means auto-return rarely fires on a phone that backgrounds often; pausing instead would be a reasonable change if that matters. Pure helpers (duration parsing, same-day comparison) are in utils/calendarIdleReturn.js with tests. --- client/src/components/CalendarWidget.jsx | 155 +++++++++++++++++++- client/src/i18n/locales/en/calendar.json | 10 +- client/src/i18n/locales/es/calendar.json | 10 +- client/src/utils/calendarIdleReturn.js | 43 ++++++ client/src/utils/calendarIdleReturn.test.js | 78 ++++++++++ client/src/utils/dateUtils.js | 3 + docs/reference/features.md | 7 + 7 files changed, 295 insertions(+), 11 deletions(-) create mode 100644 client/src/utils/calendarIdleReturn.js create mode 100644 client/src/utils/calendarIdleReturn.test.js diff --git a/client/src/components/CalendarWidget.jsx b/client/src/components/CalendarWidget.jsx index 4a588f2..2c97f41 100644 --- a/client/src/components/CalendarWidget.jsx +++ b/client/src/components/CalendarWidget.jsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { Card, Typography, Box, List, ListItem, ListItemText, Dialog, DialogTitle, DialogContent, DialogActions, Button, IconButton, Popover, ToggleButton, ToggleButtonGroup, TextField, Switch, Checkbox, FormControlLabel, Select, MenuItem, FormControl, InputLabel, Chip, Divider, CircularProgress, Alert, Tooltip } from '@mui/material'; -import { Settings, ViewModule, ViewWeek, ChevronLeft, ChevronRight, Add, Delete, Edit, Refresh, Remove, Sync, Schedule } from '@mui/icons-material'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { Card, Typography, Box, List, ListItem, ListItemText, Dialog, DialogTitle, DialogContent, DialogActions, Button, ButtonBase, IconButton, Popover, ToggleButton, ToggleButtonGroup, TextField, Switch, Checkbox, FormControlLabel, Select, MenuItem, FormControl, InputLabel, Chip, Divider, CircularProgress, Alert, Tooltip } from '@mui/material'; +import { Settings, ViewModule, ViewWeek, ChevronLeft, ChevronRight, Add, Delete, Edit, Refresh, Remove, Sync, Schedule, Today } from '@mui/icons-material'; import moment from 'moment'; import { SketchPicker } from 'react-color'; import axios from 'axios'; @@ -10,6 +10,14 @@ import { getDeviceApiBase } from '../utils/deviceName.js'; import { getEventPillPalette, getPreferredColorMode } from '../utils/colorContrast.js'; import { buildMergedDotColors, buildMergedDotBackground, describeMergedCalendars } from '../utils/calendarMergeColors.js'; import useIsMobile from '../hooks/useIsMobile.js'; +import { usePageVisibility } from '../hooks/useScreenActivity.js'; +import { + DEFAULT_IDLE_RETURN_MINUTES, + MAX_IDLE_RETURN_MINUTES, + idleReturnTimeoutMs, + isSameLocalCalendarDay, + normalizeIdleReturnMinutes, +} from '../utils/calendarIdleReturn.js'; import MonthDayCell from './MonthDayCell.jsx'; import ColorPickerPopover from './ColorPickerPopover.jsx'; import { @@ -19,6 +27,7 @@ import { formatShortDateTime, formatFullDate, formatMonthYear, + formatMonthShortYear, formatWeekdayShort, formatMonthShort, formatDayOfMonth, @@ -208,6 +217,14 @@ const CalendarWidget = ({ const [viewMode, setViewMode] = useState('month'); const [currentDate, setCurrentDate] = useState(new Date()); const [eventColors, setEventColors] = useState({ ...DEFAULT_CALENDAR_EVENT_COLORS }); + // Idle auto-return: 0 means DISABLED, not "return instantly". + // The raw input string lets the settings field accept an empty box and + // parse it as disabled without snapping back to a number on every keystroke. + const [idleReturnMinutes, setIdleReturnMinutes] = useState(DEFAULT_IDLE_RETURN_MINUTES); + const [idleReturnMinutesInput, setIdleReturnMinutesInput] = useState(String(DEFAULT_IDLE_RETURN_MINUTES)); + const [activityTick, setActivityTick] = useState(0); + const pageVisible = usePageVisibility(); + const markActivity = useCallback(() => setActivityTick((tick) => tick + 1), []); const [displaySettings, setDisplaySettings] = useState({ ...DEFAULT_CALENDAR_DISPLAY_SETTINGS }); const [dayOfWeekSettings, setDayOfWeekSettings] = useState({ ...DEFAULT_CALENDAR_DAY_OF_WEEK_SETTINGS }); const [calendarSettingsLoaded, setCalendarSettingsLoaded] = useState(false); @@ -311,6 +328,12 @@ const CalendarWidget = ({ ...settings.eventColors, }); } + + if (Object.prototype.hasOwnProperty.call(settings, 'idleReturnMinutes')) { + const normalized = normalizeIdleReturnMinutes(settings.idleReturnMinutes); + setIdleReturnMinutes(normalized); + setIdleReturnMinutesInput(normalized > 0 ? String(normalized) : ''); + } } catch (error) { console.error('Error loading calendar widget settings:', error); } finally { @@ -331,6 +354,7 @@ const CalendarWidget = ({ await axios.patch(`${API_DEVICE_URL}/settings`, { calendarWidgetSettings: { eventColors, + idleReturnMinutes, }, }); } catch (error) { @@ -344,6 +368,7 @@ const CalendarWidget = ({ API_DEVICE_URL, calendarSettingsLoaded, eventColors, + idleReturnMinutes, ]); useEffect(() => { @@ -434,6 +459,36 @@ const CalendarWidget = ({ return () => clearTimeout(timeoutId); }, [API_DEVICE_URL, activeTab, activeTabConfigJson, dayOfWeekSettings, displaySettings]); + const isViewingToday = isSameLocalCalendarDay(currentDate, new Date()); + + const goToToday = () => { + if (isViewingToday) return; + setCurrentDate(new Date()); + markActivity(); + }; + + // Idle auto-return. Any user interaction bumps activityTick, + // which restarts the countdown; unmounting or a config change clears the + // pending timeout via the cleanup below (a leak on a wall display that runs + // for months would be a real bug, not a theoretical one). While the browser + // tab is hidden we bail out entirely — the same page-visibility signal the + // rest of the app already uses — so the calendar doesn't silently jump + // around behind another tab. Auto-return moves the date only; viewMode is + // preserved so a household that deliberately chose week view isn't flipped + // back to month as a second surprise on top of the date jump. + useEffect(() => { + const timeoutMs = idleReturnTimeoutMs(idleReturnMinutes); + if (!timeoutMs) return undefined; + if (!pageVisible) return undefined; + if (isViewingToday) return undefined; + + const timeoutId = setTimeout(() => { + setCurrentDate(new Date()); + }, timeoutMs); + + return () => clearTimeout(timeoutId); + }, [activityTick, idleReturnMinutes, pageVisible, isViewingToday]); + const fetchCalendarSources = async () => { try { const response = await axios.get(`${API_BASE_URL}/api/calendar-sources`); @@ -805,6 +860,7 @@ const CalendarWidget = ({ const openCreateEventDialog = () => { const googleSources = getGoogleSources(); if (googleSources.length === 0) return; + markActivity(); const baseDay = selectedDate ? moment(selectedDate) : moment(); const start = baseDay.clone().hour(9).minute(0).second(0); const end = start.clone().add(1, 'hour'); @@ -821,6 +877,7 @@ const CalendarWidget = ({ }; const openEditEventDialog = (event) => { + markActivity(); const allDay = !!event.all_day; const startStr = allDay ? moment(event.start).format('YYYY-MM-DD') @@ -900,6 +957,7 @@ const CalendarWidget = ({ }; const handleSelectSlot = ({ start }) => { + markActivity(); const selectedDay = moment(start).startOf('day'); const dayDate = selectedDay.toDate(); const dayEvents = events @@ -916,6 +974,7 @@ const CalendarWidget = ({ }; const handleSelectEvent = (event) => { + markActivity(); const selectedDay = moment(event.start).startOf('day'); const dayDate = selectedDay.toDate(); const dayEvents = events @@ -1051,6 +1110,7 @@ const CalendarWidget = ({ }; const handleSettingsClick = (event) => { + markActivity(); setSettingsAnchor(event.currentTarget); }; @@ -1070,6 +1130,7 @@ const CalendarWidget = ({ if (newViewMode === null || !TAB_CALENDAR_VIEW_MODES.has(newViewMode)) return; // Optimistic UI: switch instantly, then persist in the background. + markActivity(); setViewMode(newViewMode); void persistViewModeForTab(activeTab, newViewMode); }; @@ -1079,6 +1140,7 @@ const CalendarWidget = ({ }; const handlePreviousPeriod = () => { + markActivity(); if (viewMode === 'month') { const newDate = new Date(currentDate); if (isRollingMonthView) { @@ -1097,6 +1159,7 @@ const CalendarWidget = ({ }; const handleNextPeriod = () => { + markActivity(); if (viewMode === 'month') { const newDate = new Date(currentDate); if (isRollingMonthView) { @@ -1137,7 +1200,7 @@ const CalendarWidget = ({ return formatDateRangeLabel(start, start.clone().add(monthViewWeeksToShow * 7 - 1, 'days')); } - return formatMonthYear(currentDate); + return isMobile ? formatMonthShortYear(currentDate) : formatMonthYear(currentDate); } else { const startOfWeek = getWeekStartDate(); const endOfWeek = startOfWeek.clone().add(6, 'days'); @@ -1207,9 +1270,37 @@ const CalendarWidget = ({ > - - 📅 {getCurrentPeriodLabel()} - + {/* The period label doubles as the go-to-today control: on a phone this is the only Today affordance, and on + desktop the icon button below keeps it discoverable. Disabled + when already on today so it neither reacts nor advertises. */} + + + + {isMobile ? '' : '📅 '}{getCurrentPeriodLabel()} + {isViewingToday && ` · ${t('calendar:widget.todayIndicator')}`} + + + + + {/* Desktop-only redundant affordance: on phones the label above is + the control, but a tappable heading isn't self-evidently + tappable, so keep the icon visible from `sm` up. span wrapper: + MUI Tooltips need a non-disabled child to receive pointer + events, and this button is disabled on today. */} + + + + + + + {t('calendar:settings.idleReturnHeading')} + + {t('calendar:settings.idleReturnHelp')} + + { + const raw = e.target.value; + setIdleReturnMinutesInput(raw); + setIdleReturnMinutes(normalizeIdleReturnMinutes(raw)); + }} + slotProps={{ + htmlInput: { + min: 0, + max: MAX_IDLE_RETURN_MINUTES, + step: 1, + inputMode: 'numeric', + }, + }} + helperText={t('calendar:settings.idleReturnDisabledHint')} + sx={{ mb: 2 }} + /> + + + {t('calendar:settings.defaultColors')} diff --git a/client/src/i18n/locales/en/calendar.json b/client/src/i18n/locales/en/calendar.json index c7de8ff..c242c7f 100644 --- a/client/src/i18n/locales/en/calendar.json +++ b/client/src/i18n/locales/en/calendar.json @@ -9,7 +9,9 @@ "weekView": "week view", "addEvent": "Add event", "editEvent": "Edit event", - "deleteEvent": "Delete event" + "deleteEvent": "Delete event", + "goToToday": "Go to today", + "todayIndicator": "Today" }, "event": { "calendar": "Calendar", @@ -70,7 +72,11 @@ "defaultColors": "Default Event Colors", "eventBackground": "Event Background Color", "eventText": "Event Text Color", - "resetToDefault": "Reset to Default" + "resetToDefault": "Reset to Default", + "idleReturnHeading": "Auto-return to today", + "idleReturnHelp": "After this many minutes without any interaction, the calendar jumps back to today. The view (month or week) is left alone.", + "idleReturnMinutes": "Minutes idle", + "idleReturnDisabledHint": "Set to 0 (or leave empty) to turn auto-return off." }, "refresh": { "disabled": "Disabled", diff --git a/client/src/i18n/locales/es/calendar.json b/client/src/i18n/locales/es/calendar.json index fa0017e..d865158 100644 --- a/client/src/i18n/locales/es/calendar.json +++ b/client/src/i18n/locales/es/calendar.json @@ -9,7 +9,9 @@ "weekView": "vista de semana", "addEvent": "Añadir evento", "editEvent": "Editar evento", - "deleteEvent": "Eliminar evento" + "deleteEvent": "Eliminar evento", + "goToToday": "Ir a hoy", + "todayIndicator": "Hoy" }, "event": { "calendar": "Calendario", @@ -70,7 +72,11 @@ "defaultColors": "Colores predeterminados de los eventos", "eventBackground": "Color de fondo del evento", "eventText": "Color del texto del evento", - "resetToDefault": "Restablecer valores predeterminados" + "resetToDefault": "Restablecer valores predeterminados", + "idleReturnHeading": "Volver a hoy automáticamente", + "idleReturnHelp": "Tras estos minutos sin ninguna interacción, el calendario vuelve al día de hoy. La vista (mes o semana) se mantiene.", + "idleReturnMinutes": "Minutos sin actividad", + "idleReturnDisabledHint": "Pon 0 (o déjalo vacío) para desactivarlo." }, "refresh": { "disabled": "Desactivado", diff --git a/client/src/utils/calendarIdleReturn.js b/client/src/utils/calendarIdleReturn.js new file mode 100644 index 0000000..917c696 --- /dev/null +++ b/client/src/utils/calendarIdleReturn.js @@ -0,0 +1,43 @@ +// Helpers behind the calendar's "return to today after idle" feature. Wall +// displays that sit on last month look current and are not; a bounded idle +// timer swings the view back to today after the household has stopped +// touching the calendar for a while. + +export const DEFAULT_IDLE_RETURN_MINUTES = 20; +export const MIN_IDLE_RETURN_MINUTES = 1; +export const MAX_IDLE_RETURN_MINUTES = 240; + +// Parses the configured minutes into an integer >= 0. A value of 0 (or an +// empty / negative / non-numeric input) means DISABLED — auto-return is off +// entirely rather than "return immediately", which would make the calendar +// impossible to navigate away from today. +export const normalizeIdleReturnMinutes = (raw) => { + if (raw === '' || raw === null || raw === undefined) return 0; + const parsed = typeof raw === 'number' ? raw : Number(raw); + if (!Number.isFinite(parsed)) return 0; + const floored = Math.floor(parsed); + if (floored <= 0) return 0; + return Math.min(MAX_IDLE_RETURN_MINUTES, Math.max(MIN_IDLE_RETURN_MINUTES, floored)); +}; + +export const isIdleReturnEnabled = (minutes) => normalizeIdleReturnMinutes(minutes) > 0; + +// Millisecond timeout to hand to setTimeout, or null when disabled — callers +// use null to skip arming the timer at all. +export const idleReturnTimeoutMs = (minutes) => { + const n = normalizeIdleReturnMinutes(minutes); + return n > 0 ? n * 60 * 1000 : null; +}; + +// True when two Date-likes represent the same local calendar day. Guards the +// button's disabled state and prevents the idle timer from firing a no-op +// setCurrentDate when the widget is already on today. +export const isSameLocalCalendarDay = (a, b) => { + if (a == null || b == null) return false; + const da = a instanceof Date ? a : new Date(a); + const db = b instanceof Date ? b : new Date(b); + if (Number.isNaN(da.getTime()) || Number.isNaN(db.getTime())) return false; + return da.getFullYear() === db.getFullYear() + && da.getMonth() === db.getMonth() + && da.getDate() === db.getDate(); +}; diff --git a/client/src/utils/calendarIdleReturn.test.js b/client/src/utils/calendarIdleReturn.test.js new file mode 100644 index 0000000..5892bce --- /dev/null +++ b/client/src/utils/calendarIdleReturn.test.js @@ -0,0 +1,78 @@ +import { describe, it, expect } from 'vitest'; +import { + DEFAULT_IDLE_RETURN_MINUTES, + MAX_IDLE_RETURN_MINUTES, + MIN_IDLE_RETURN_MINUTES, + normalizeIdleReturnMinutes, + isIdleReturnEnabled, + idleReturnTimeoutMs, + isSameLocalCalendarDay, +} from './calendarIdleReturn.js'; + +describe('normalizeIdleReturnMinutes', () => { + it('treats empty, null, undefined, NaN, and non-numeric as disabled (0)', () => { + expect(normalizeIdleReturnMinutes('')).toBe(0); + expect(normalizeIdleReturnMinutes(null)).toBe(0); + expect(normalizeIdleReturnMinutes(undefined)).toBe(0); + expect(normalizeIdleReturnMinutes('abc')).toBe(0); + expect(normalizeIdleReturnMinutes(Number.NaN)).toBe(0); + expect(normalizeIdleReturnMinutes(Infinity)).toBe(0); + }); + + it('treats 0 and negative values as disabled', () => { + expect(normalizeIdleReturnMinutes(0)).toBe(0); + expect(normalizeIdleReturnMinutes('0')).toBe(0); + expect(normalizeIdleReturnMinutes(-5)).toBe(0); + expect(normalizeIdleReturnMinutes('-5')).toBe(0); + }); + + it('clamps to [MIN, MAX] and floors fractional inputs', () => { + expect(normalizeIdleReturnMinutes(20)).toBe(20); + expect(normalizeIdleReturnMinutes('45')).toBe(45); + expect(normalizeIdleReturnMinutes(20.9)).toBe(20); + expect(normalizeIdleReturnMinutes(1)).toBe(MIN_IDLE_RETURN_MINUTES); + expect(normalizeIdleReturnMinutes(9999)).toBe(MAX_IDLE_RETURN_MINUTES); + }); +}); + +describe('isIdleReturnEnabled', () => { + it('is false only when normalization produces 0', () => { + expect(isIdleReturnEnabled(0)).toBe(false); + expect(isIdleReturnEnabled('')).toBe(false); + expect(isIdleReturnEnabled(null)).toBe(false); + expect(isIdleReturnEnabled(DEFAULT_IDLE_RETURN_MINUTES)).toBe(true); + expect(isIdleReturnEnabled(1)).toBe(true); + }); +}); + +describe('idleReturnTimeoutMs', () => { + it('returns null when disabled, and minutes-in-ms otherwise', () => { + expect(idleReturnTimeoutMs(0)).toBeNull(); + expect(idleReturnTimeoutMs('')).toBeNull(); + expect(idleReturnTimeoutMs(20)).toBe(20 * 60 * 1000); + expect(idleReturnTimeoutMs('1')).toBe(60 * 1000); + }); +}); + +describe('isSameLocalCalendarDay', () => { + it('is true for two Dates on the same local day, regardless of time', () => { + const morning = new Date(2026, 7, 28, 6, 30); + const evening = new Date(2026, 7, 28, 23, 59); + expect(isSameLocalCalendarDay(morning, evening)).toBe(true); + }); + + it('is false across a midnight boundary', () => { + const late = new Date(2026, 7, 28, 23, 59); + const early = new Date(2026, 7, 29, 0, 1); + expect(isSameLocalCalendarDay(late, early)).toBe(false); + }); + + it('coerces Date-likes and rejects invalid inputs', () => { + expect(isSameLocalCalendarDay(null, new Date())).toBe(false); + expect(isSameLocalCalendarDay(new Date(), undefined)).toBe(false); + expect(isSameLocalCalendarDay('not a date', new Date())).toBe(false); + expect( + isSameLocalCalendarDay('2026-08-28T10:00:00', '2026-08-28T22:00:00'), + ).toBe(true); + }); +}); diff --git a/client/src/utils/dateUtils.js b/client/src/utils/dateUtils.js index e935775..c8be91d 100644 --- a/client/src/utils/dateUtils.js +++ b/client/src/utils/dateUtils.js @@ -83,6 +83,9 @@ export const formatFullDate = (value) => /** "August 2026" — month and year, for calendar headers. */ export const formatMonthYear = (value) => formatWith({ year: 'numeric', month: 'long' }, value); +/** "Aug 2026" — short month and year, for narrow calendar headers. */ +export const formatMonthShortYear = (value) => formatWith({ year: 'numeric', month: 'short' }, value); + /** "Thu" — abbreviated weekday. */ export const formatWeekdayShort = (value) => formatWith({ weekday: 'short' }, value); diff --git a/docs/reference/features.md b/docs/reference/features.md index 66bc5b6..f1b4794 100644 --- a/docs/reference/features.md +++ b/docs/reference/features.md @@ -188,6 +188,13 @@ in `server/index.js`. calendar colors, so it keeps answering "which calendars is this on?" - **Per-event Google colors** (PR #133): an event individually recolored in Google keeps that color in HomeGlow instead of inheriting its calendar's. +- **Return to today**: the period label is a button — tap it to jump back. Inert + when already there. Desktop also gets a 📅 button; on a phone the header has no + room for one. +- **Idle auto-return** (`calendarWidgetSettings.idleReturnMinutes`, per device, + default 20 minutes, 0 disables): returns to today after that long without + interaction, so a wall display left on last month stops looking current. Resets + the date only, not the view. Sync resolves the event's `colorId` to a hex through Google's `/colors` palette (cached 24h) and stores it in the existing `raw_data` column, which `getCachedEvents` surfaces as `event_color`. Every view prefers From e15a36f7eb45a5ed47b23fa158bc1fd7f3f4052d Mon Sep 17 00:00:00 2001 From: mrramam Date: Sat, 29 Aug 2026 10:56:38 -0700 Subject: [PATCH 2/3] fix(calendar): remove stale 'today' assertion from header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The isViewingToday flag is derived from currentDate, which is set once at mount and never rolls over at midnight. After midnight, the header would claim a stale date is today, and the Today control would stay disabled exactly when the user most wants to tap it. Remove the '· Today' indicator appended to the period label, drop the disabled prop and its associated Tooltip suppression props and dead Mui-disabled styling from both the label ButtonBase and the desktop Today IconButton, and remove the Box wrapper that existed solely to give MUI's Tooltip a non-disabled child (moving the responsive display style directly onto the IconButton). Delete the widget.todayIndicator i18n key from en and es locales. goToToday's early-return guard and isViewingToday itself are preserved — the idle auto-return effect still uses the flag and staleness there is harmless. --- client/src/components/CalendarWidget.jsx | 41 +++++++----------------- client/src/i18n/locales/en/calendar.json | 3 +- client/src/i18n/locales/es/calendar.json | 3 +- 3 files changed, 14 insertions(+), 33 deletions(-) diff --git a/client/src/components/CalendarWidget.jsx b/client/src/components/CalendarWidget.jsx index 2c97f41..8901239 100644 --- a/client/src/components/CalendarWidget.jsx +++ b/client/src/components/CalendarWidget.jsx @@ -1271,33 +1271,24 @@ const CalendarWidget = ({ {/* The period label doubles as the go-to-today control: on a phone this is the only Today affordance, and on - desktop the icon button below keeps it discoverable. Disabled - when already on today so it neither reacts nor advertises. */} - + desktop the icon button below keeps it discoverable. */} + {isMobile ? '' : '📅 '}{getCurrentPeriodLabel()} - {isViewingToday && ` · ${t('calendar:widget.todayIndicator')}`} @@ -1312,23 +1303,15 @@ const CalendarWidget = ({ {/* Desktop-only redundant affordance: on phones the label above is the control, but a tappable heading isn't self-evidently - tappable, so keep the icon visible from `sm` up. span wrapper: - MUI Tooltips need a non-disabled child to receive pointer - events, and this button is disabled on today. */} - - - - - + + diff --git a/client/src/i18n/locales/en/calendar.json b/client/src/i18n/locales/en/calendar.json index c242c7f..a3b2b3f 100644 --- a/client/src/i18n/locales/en/calendar.json +++ b/client/src/i18n/locales/en/calendar.json @@ -10,8 +10,7 @@ "addEvent": "Add event", "editEvent": "Edit event", "deleteEvent": "Delete event", - "goToToday": "Go to today", - "todayIndicator": "Today" + "goToToday": "Go to today" }, "event": { "calendar": "Calendar", diff --git a/client/src/i18n/locales/es/calendar.json b/client/src/i18n/locales/es/calendar.json index d865158..e894538 100644 --- a/client/src/i18n/locales/es/calendar.json +++ b/client/src/i18n/locales/es/calendar.json @@ -10,8 +10,7 @@ "addEvent": "Añadir evento", "editEvent": "Editar evento", "deleteEvent": "Eliminar evento", - "goToToday": "Ir a hoy", - "todayIndicator": "Hoy" + "goToToday": "Ir a hoy" }, "event": { "calendar": "Calendario", From 95c598ce22448e81f5a6b9b94fdba72fd10674d1 Mon Sep 17 00:00:00 2001 From: mrramam Date: Sat, 29 Aug 2026 16:42:21 -0700 Subject: [PATCH 3/3] fix(calendar): evaluate today at click time in the today control The today control compared currentDate against a new Date() captured at render. A display with refresh disabled can cross midnight without re-rendering, leaving the comparison stale-true and the control inert at the moment it is actually needed. Evaluate now inside the handler, and mark activity on every tap since the user did interact. --- client/src/components/CalendarWidget.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/components/CalendarWidget.jsx b/client/src/components/CalendarWidget.jsx index 8901239..4b00b2a 100644 --- a/client/src/components/CalendarWidget.jsx +++ b/client/src/components/CalendarWidget.jsx @@ -462,8 +462,10 @@ const CalendarWidget = ({ const isViewingToday = isSameLocalCalendarDay(currentDate, new Date()); const goToToday = () => { - if (isViewingToday) return; - setCurrentDate(new Date()); + const now = new Date(); + if (!isSameLocalCalendarDay(currentDate, now)) { + setCurrentDate(now); + } markActivity(); };