When already viewing today, clicking Today now snaps the selected hour to whatever hour would be default for the current wall-clock time (same as a fresh visit to /vu); when viewing another date it still just jumps to today while preserving the current hour, as before. The link's href is built from the same todayTargetHour() decision as the click handler, so right-click -> open in new tab lands on the same target instead of following a stale href. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbHcmjnqQPJZ1i2yoebmp5
This commit is contained in:
+18
-1
@@ -56,8 +56,25 @@ export function setDate(date: string): void {
|
|||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which hour the "Today" link should point to: if we're already viewing
|
||||||
|
* today, the current time-of-day default (as if freshly visiting `/vu`
|
||||||
|
* right now); otherwise the hour currently selected, unchanged. Shared by
|
||||||
|
* `goToToday()` and the link's `href` (`day-nav.ts`) so a plain click and
|
||||||
|
* a right-click-open-in-new-tab land on the same target.
|
||||||
|
*/
|
||||||
|
export function todayTargetHour(): HourId {
|
||||||
|
return state.date === todayIso() ? defaultHourForTime(new Date()) : state.selectedHour;
|
||||||
|
}
|
||||||
|
|
||||||
export function goToToday(): void {
|
export function goToToday(): void {
|
||||||
setDate(todayIso());
|
const today = todayIso();
|
||||||
|
const hour = todayTargetHour();
|
||||||
|
if (state.date === today) {
|
||||||
|
setSelectedHour(hour);
|
||||||
|
} else {
|
||||||
|
setDate(today);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addDays(date: string, days: number): string {
|
export function addDays(date: string, days: number): string {
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { getState, shiftDate, goToToday, addDays, todayIso } from '../app-state';
|
import { getState, shiftDate, goToToday, addDays, todayIso, todayTargetHour } from '../app-state';
|
||||||
import { resolveDay } from '../calendar';
|
import { resolveDay } from '../calendar';
|
||||||
import { resolveEveningDay } from '../calendar/vespers';
|
import { resolveEveningDay } from '../calendar/vespers';
|
||||||
import { getDayLabel, getVespersStatusLabel } from '../calendar/day-label';
|
import { getDayLabel, getVespersStatusLabel } from '../calendar/day-label';
|
||||||
@@ -26,7 +26,7 @@ export function renderDayNav(container: HTMLElement): void {
|
|||||||
const vespersStatus = selectedHour === 'vespers' ? getVespersStatusLabel(day) : undefined;
|
const vespersStatus = selectedHour === 'vespers' ? getVespersStatusLabel(day) : undefined;
|
||||||
const prevUrl = urlFor(addDays(date, -1), selectedHour, languages);
|
const prevUrl = urlFor(addDays(date, -1), selectedHour, languages);
|
||||||
const nextUrl = urlFor(addDays(date, 1), selectedHour, languages);
|
const nextUrl = urlFor(addDays(date, 1), selectedHour, languages);
|
||||||
const todayUrl = urlFor(todayIso(), selectedHour, languages);
|
const todayUrl = urlFor(todayIso(), todayTargetHour(), languages);
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<nav class="day-nav" aria-label="Day navigation">
|
<nav class="day-nav" aria-label="Day navigation">
|
||||||
<a href="${prevUrl}" class="day-nav-btn" data-action="prev" aria-label="Previous day">←</a>
|
<a href="${prevUrl}" class="day-nav-btn" data-action="prev" aria-label="Previous day">←</a>
|
||||||
|
|||||||
+68
-2
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
import { defaultHourForTime } from '../src/app-state';
|
import { defaultHourForTime, goToToday, getState, setDate, setSelectedHour, todayTargetHour } from '../src/app-state';
|
||||||
|
|
||||||
function at(hour: number, minute = 0): Date {
|
function at(hour: number, minute = 0): Date {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
@@ -37,3 +37,69 @@ describe('defaultHourForTime', () => {
|
|||||||
expect(defaultHourForTime(at(hour, minute))).toBe(expected);
|
expect(defaultHourForTime(at(hour, minute))).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('goToToday', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date(2026, 7, 28, 10, 0, 0)); // 2026-08-28 10:00 local -> terce
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('jumps to today and preserves the current hour when on a different date', () => {
|
||||||
|
setDate('2026-08-01');
|
||||||
|
setSelectedHour('vespers');
|
||||||
|
|
||||||
|
goToToday();
|
||||||
|
|
||||||
|
expect(getState().date).toBe('2026-08-28');
|
||||||
|
expect(getState().selectedHour).toBe('vespers');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('snaps the hour to the current time-of-day default when already on today', () => {
|
||||||
|
setDate('2026-08-28');
|
||||||
|
setSelectedHour('vespers');
|
||||||
|
|
||||||
|
goToToday();
|
||||||
|
|
||||||
|
expect(getState().date).toBe('2026-08-28');
|
||||||
|
expect(getState().selectedHour).toBe('terce');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is idempotent-safe: a second click after jumping to today snaps the hour', () => {
|
||||||
|
setDate('2026-08-01');
|
||||||
|
setSelectedHour('vespers');
|
||||||
|
|
||||||
|
goToToday(); // -> today, hour still 'vespers'
|
||||||
|
goToToday(); // -> already today, snaps hour to time-of-day default
|
||||||
|
|
||||||
|
expect(getState().date).toBe('2026-08-28');
|
||||||
|
expect(getState().selectedHour).toBe('terce');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('todayTargetHour', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date(2026, 7, 28, 10, 0, 0)); // 2026-08-28 10:00 local -> terce
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches goToToday, so the Today link href agrees with its click behavior', () => {
|
||||||
|
// Different date: href should preserve the current hour, same as goToToday().
|
||||||
|
setDate('2026-08-01');
|
||||||
|
setSelectedHour('vespers');
|
||||||
|
expect(todayTargetHour()).toBe('vespers');
|
||||||
|
|
||||||
|
// Already on today: href should point at the time-of-day default,
|
||||||
|
// same as goToToday() — this is what a right-click "open in new tab"
|
||||||
|
// relies on, since it follows the href without running the click handler.
|
||||||
|
setDate('2026-08-28');
|
||||||
|
expect(todayTargetHour()).toBe('terce');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user