diff --git a/frontend/src/components/stats/SleepTimetable.tsx b/frontend/src/components/stats/SleepTimetable.tsx index 064dfd5..fe929d2 100644 --- a/frontend/src/components/stats/SleepTimetable.tsx +++ b/frontend/src/components/stats/SleepTimetable.tsx @@ -1,6 +1,6 @@ import { useMemo, useState, useEffect, Fragment } from 'react'; import { useStats } from '../../hooks/useStats'; -import { timeToMinutes } from '../../utils/time'; +import { timeToMinutes, adjustForTimezone } from '../../utils/time'; interface TimetableProps { from_date: string; @@ -106,8 +106,8 @@ const Timetable = ({ from_date, to_date, day_start, day_end }: TimetableProps) = return timetable.slots.some((slot) => { if (slot.date !== date || !slot.sleeping) return false; - const sStart = timeToMinutes(slot.slot_start); - const sEnd = timeToMinutes(slot.slot_end); + const sStart = adjustForTimezone(timeToMinutes(slot.slot_start)); + const sEnd = adjustForTimezone(timeToMinutes(slot.slot_end)); if (sEnd <= sStart) { const overlap1 = Math.max(sStart, intervalStart) < Math.min(1440, intervalEnd); diff --git a/frontend/src/utils/time.ts b/frontend/src/utils/time.ts index bb1107e..a06b79f 100644 --- a/frontend/src/utils/time.ts +++ b/frontend/src/utils/time.ts @@ -68,7 +68,11 @@ export function formatElapsedTime(utcTimestamp: string): string { * @returns formatted datetime string */ export function formatDateTime(date: Date | string): string { - const d = typeof date === 'string' ? new Date(date) : date; + let dateToParse = date; + if (typeof date === 'string' && !date.endsWith('Z') && !date.includes('+')) { + dateToParse = date + 'Z'; + } + const d = typeof dateToParse === 'string' ? new Date(dateToParse) : dateToParse; return d.toLocaleString('sv-SE', { year: 'numeric', month: '2-digit', diff --git a/frontend/tests/unit/components/stats/SleepTimetable.test.tsx b/frontend/tests/unit/components/stats/SleepTimetable.test.tsx index 1c0af74..5d10b70 100644 --- a/frontend/tests/unit/components/stats/SleepTimetable.test.tsx +++ b/frontend/tests/unit/components/stats/SleepTimetable.test.tsx @@ -62,8 +62,8 @@ describe('SleepTimetable', () => { fireEvent(window, new Event('resize')); // Check for 3-day interval tabs - // In reversed order (25, 24, 23...), the first group is Fri - Wed - expect(screen.getByRole('button', { name: /Fri \d+ - Wed \d+/i })).toBeInTheDocument(); + // In reversed order (25, 24, 23...), the first group is Wed - Fri + expect(screen.getByRole('button', { name: /Wed \d+ - Fri \d+/i })).toBeInTheDocument(); }); it('renders in small mode (single-day tabs and 1-column grid)', () => { diff --git a/frontend/tests/unit/time.test.ts b/frontend/tests/unit/time.test.ts index c25cbc0..a2ef6be 100644 --- a/frontend/tests/unit/time.test.ts +++ b/frontend/tests/unit/time.test.ts @@ -11,6 +11,7 @@ import { timeToMinutes, adjustForTimezone, getLocalOffsetFromReference, + formatDateTime, } from '../../src/utils/time'; describe('formatMinutesAfter', () => { @@ -125,6 +126,21 @@ describe('adjustForTimezone', () => { }); }); +describe('formatDateTime', () => { + it('treats string without Z as UTC', () => { + const dateStr = '2024-01-15T10:00:00'; + const dateWithZ = '2024-01-15T10:00:00Z'; + expect(formatDateTime(dateStr)).toBe(formatDateTime(dateWithZ)); + }); + + it('formats a Date object correctly', () => { + const date = new Date('2024-01-15T10:00:00Z'); + // The output depends on the local timezone, so we check for part of it + const result = formatDateTime(date); + expect(result).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/); + }); +}); + describe('getLocalOffsetFromReference', () => { it('returns local offset from reference in minutes', () => { const utcMins = 600; // 10:00 UTC diff --git a/plan.md b/plan.md deleted file mode 100644 index b1fa2a8..0000000 --- a/plan.md +++ /dev/null @@ -1,22 +0,0 @@ -# Sleep Timetable Bug Fixes - -## Identified Issues -1. **Pre-selection**: Component doesn't always pre-select the latest date across all tiers. -2. **Grid Layout (Medium Tier)**: Grid columns are hardcoded to 3, causing breakage when fewer than 3 days are displayed at the end of a range. -3. **Selector Information**: Tabs only show weekday names, making it hard to distinguish dates. - -## Implementation Plan - -### 1. Fix Pre-selection -- Add a `useEffect` to set `selectedDayIndex` to the last index of the `days` array whenever the `days` list is initialized or updated. This ensures "today" (the latest date) is selected by default. - -### 2. Fix Grid Alignment -- Refactor the `gridTemplateColumns` logic for both the header and time grid. -- Instead of hardcoding tiers (e.g., `repeat(3, 1fr)`), use the actual number of days in the current view: `4rem repeat(${visibleDays.length}, 1fr)`. This allows the grid to gracefully handle 1, 2, or 3 columns. - -### 3. Enhance Selector Labels -- Implement a `formatDayLabel` helper that combines the short weekday name and the day of the month (e.g., "FRI 24"). -- Update `tabGroups` generation for both 'medium' (range) and 'small' (single day) tiers to use this descriptive label. - -### 4. UI Compactness -- Adjust spacing (margins/paddings) in the mobile/medium view to reduce overall component height and keep selectors closer to the grid. diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..bab1e86 --- /dev/null +++ b/todo.md @@ -0,0 +1,5 @@ +# Things I need to tell my AI to do + +- [ ] Check so that all tests actually make sense, maybe some are now obsolete, and some are missing +- [ ] Add a test coverage metric to both frontend and backend so that we can check how much of the code is tested. I would like >=90% +- [ ] Go over Claude's slop-cleanup-stuff