Skip to content

Commit 640383b

Browse files
committed
feat(web): implement getNowLinePosition utility and refactor Agenda components
- Introduced `getNowLinePosition` utility to calculate the pixel position of the now line based on the current time, accounting for fractional minutes. - Refactored `AgendaEvents` and `NowLine` components to utilize the new utility for positioning, improving code clarity and maintainability. - Added comprehensive tests for `getNowLinePosition` to ensure accurate positioning across various time scenarios.
1 parent b6e8b97 commit 640383b

File tree

4 files changed

+165
-13
lines changed

4 files changed

+165
-13
lines changed

packages/web/src/views/Day/components/Agenda/Events/AgendaEvent/AgendaEvents.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1-
import {
2-
MINUTES_PER_SLOT,
3-
SLOT_HEIGHT,
4-
} from "@web/views/Day/constants/day.constants";
1+
import { SLOT_HEIGHT } from "@web/views/Day/constants/day.constants";
52
import { useDayEvents } from "@web/views/Day/data/day.data";
63
import { useDateInView } from "@web/views/Day/hooks/navigation/useDateInView";
4+
import { getNowLinePosition } from "@web/views/Day/util/agenda/agenda.util";
75
import { AgendaSkeleton } from "../../AgendaSkeleton/AgendaSkeleton";
86
import { AgendaEvent } from "./AgendaEvent";
97

108
export const AgendaEvents = () => {
119
const dateInView = useDateInView();
1210
const { events, isLoading } = useDayEvents(dateInView);
1311
const currentTime = new Date();
14-
const currentHour = currentTime.getHours();
15-
const currentMinute = currentTime.getMinutes();
1612

1713
// Filter out all-day events and sort timed events by start time for consistent TAB order
1814
const timedEvents = events
@@ -36,7 +32,7 @@ export const AgendaEvents = () => {
3632
<div
3733
className="border-red absolute right-0 left-0 z-30 border-t-2"
3834
style={{
39-
top: `${(currentHour * 4 + Math.floor(currentMinute / MINUTES_PER_SLOT)) * SLOT_HEIGHT}px`,
35+
top: `${getNowLinePosition(currentTime)}px`,
4036
}}
4137
/>
4238

packages/web/src/views/Day/components/Agenda/NowLine/NowLine.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useEffect, useState } from "react";
2-
import { MINUTES_PER_SLOT } from "@web/views/Day/constants/day.constants";
3-
import { SLOT_HEIGHT } from "@web/views/Day/constants/day.constants";
4-
import { getAgendaEventTime } from "@web/views/Day/util/agenda/agenda.util";
2+
import {
3+
getAgendaEventTime,
4+
getNowLinePosition,
5+
} from "@web/views/Day/util/agenda/agenda.util";
56
import { setupMinuteSync } from "@web/views/Day/util/time/time.util";
67

78
export const NowLine = ({
@@ -10,8 +11,6 @@ export const NowLine = ({
1011
nowLineRef: React.RefObject<HTMLDivElement>;
1112
}) => {
1213
const [currentTime, setCurrentTime] = useState(new Date());
13-
const currentHour = currentTime.getHours();
14-
const currentMinute = currentTime.getMinutes();
1514

1615
useEffect(() => {
1716
const cleanup = setupMinuteSync(() => {
@@ -27,7 +26,7 @@ export const NowLine = ({
2726
data-now-marker="true"
2827
className="absolute right-0 left-0 z-40 border-t-2 text-yellow-300"
2928
style={{
30-
top: `${(currentHour * 4 + Math.floor(currentMinute / MINUTES_PER_SLOT)) * SLOT_HEIGHT}px`,
29+
top: `${getNowLinePosition(currentTime)}px`,
3130
}}
3231
>
3332
<div className="absolute -top-1 -left-2 h-2 w-4 rounded-full"></div>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { MINUTES_PER_SLOT, SLOT_HEIGHT } from "../../constants/day.constants";
2+
import { getNowLinePosition } from "./agenda.util";
3+
4+
describe("agenda.util", () => {
5+
describe("getNowLinePosition", () => {
6+
it("should position at exact hour marks correctly", () => {
7+
// 3:00pm (15:00 in 24-hour format)
8+
const date = new Date();
9+
date.setHours(15, 0, 0, 0);
10+
const position = getNowLinePosition(date);
11+
// 15 hours * 4 slots/hour * 20px/slot = 1200px
12+
expect(position).toBe(1200);
13+
});
14+
15+
it("should position at quarter-hour marks correctly", () => {
16+
// 3:15pm (15:15 in 24-hour format)
17+
const date = new Date();
18+
date.setHours(15, 15, 0, 0);
19+
const position = getNowLinePosition(date);
20+
// (15 * 4 + 15/15) * 20 = (60 + 1) * 20 = 1220px
21+
expect(position).toBe(1220);
22+
});
23+
24+
it("should position at half-hour marks correctly", () => {
25+
// 3:30pm (15:30 in 24-hour format)
26+
const date = new Date();
27+
date.setHours(15, 30, 0, 0);
28+
const position = getNowLinePosition(date);
29+
// (15 * 4 + 30/15) * 20 = (60 + 2) * 20 = 1240px
30+
expect(position).toBe(1240);
31+
});
32+
33+
it("should position at three-quarter hour marks correctly", () => {
34+
// 3:45pm (15:45 in 24-hour format)
35+
const date = new Date();
36+
date.setHours(15, 45, 0, 0);
37+
const position = getNowLinePosition(date);
38+
// (15 * 4 + 45/15) * 20 = (60 + 3) * 20 = 1260px
39+
expect(position).toBe(1260);
40+
});
41+
42+
it("should position at 3:26pm correctly (fractional minutes)", () => {
43+
// 3:26pm (15:26 in 24-hour format)
44+
const date = new Date();
45+
date.setHours(15, 26, 0, 0);
46+
const position = getNowLinePosition(date);
47+
// (15 * 4 + 26/15) * 20 = (60 + 1.733...) * 20 ≈ 1234.67px
48+
const expectedPosition = (15 * 4 + 26 / MINUTES_PER_SLOT) * SLOT_HEIGHT;
49+
expect(position).toBeCloseTo(expectedPosition, 2);
50+
// Verify it's approximately 43% between 3pm (1200px) and 4pm (1280px)
51+
// 26/60 = 0.433... of the hour
52+
// 1200 + (0.433... * 80) ≈ 1234.67px
53+
expect(position).toBeCloseTo(1234.67, 2);
54+
});
55+
56+
it("should position at single minutes correctly", () => {
57+
// 3:01pm (15:01 in 24-hour format)
58+
const date = new Date();
59+
date.setHours(15, 1, 0, 0);
60+
const position = getNowLinePosition(date);
61+
// (15 * 4 + 1/15) * 20 = (60 + 0.0667) * 20 ≈ 1201.33px
62+
const expectedPosition = (15 * 4 + 1 / MINUTES_PER_SLOT) * SLOT_HEIGHT;
63+
expect(position).toBeCloseTo(expectedPosition, 2);
64+
});
65+
66+
it("should position at 3:07pm correctly", () => {
67+
// 3:07pm (15:07 in 24-hour format)
68+
const date = new Date();
69+
date.setHours(15, 7, 0, 0);
70+
const position = getNowLinePosition(date);
71+
// (15 * 4 + 7/15) * 20 = (60 + 0.4667) * 20 ≈ 1209.33px
72+
const expectedPosition = (15 * 4 + 7 / MINUTES_PER_SLOT) * SLOT_HEIGHT;
73+
expect(position).toBeCloseTo(expectedPosition, 2);
74+
});
75+
76+
it("should position at midnight correctly", () => {
77+
// 0:00 (midnight)
78+
const date = new Date();
79+
date.setHours(0, 0, 0, 0);
80+
const position = getNowLinePosition(date);
81+
// 0 * 4 * 20 = 0px
82+
expect(position).toBe(0);
83+
});
84+
85+
it("should position at end of day correctly", () => {
86+
// 23:59 (end of day)
87+
const date = new Date();
88+
date.setHours(23, 59, 0, 0);
89+
const position = getNowLinePosition(date);
90+
// (23 * 4 + 59/15) * 20 = (92 + 3.933...) * 20 ≈ 1918.67px
91+
const expectedPosition = (23 * 4 + 59 / MINUTES_PER_SLOT) * SLOT_HEIGHT;
92+
expect(position).toBeCloseTo(expectedPosition, 2);
93+
});
94+
95+
it("should position at noon correctly", () => {
96+
// 12:00pm (12:00 in 24-hour format)
97+
const date = new Date();
98+
date.setHours(12, 0, 0, 0);
99+
const position = getNowLinePosition(date);
100+
// 12 * 4 * 20 = 960px
101+
expect(position).toBe(960);
102+
});
103+
104+
it("should position at 1:00am correctly", () => {
105+
// 1:00am (1:00 in 24-hour format)
106+
const date = new Date();
107+
date.setHours(1, 0, 0, 0);
108+
const position = getNowLinePosition(date);
109+
// 1 * 4 * 20 = 80px
110+
expect(position).toBe(80);
111+
});
112+
113+
it("should handle seconds correctly (ignores seconds)", () => {
114+
// 3:26:30pm - should position same as 3:26pm
115+
const date1 = new Date();
116+
date1.setHours(15, 26, 30, 0);
117+
const position1 = getNowLinePosition(date1);
118+
119+
const date2 = new Date();
120+
date2.setHours(15, 26, 0, 0);
121+
const position2 = getNowLinePosition(date2);
122+
123+
// Both should be the same since we only use hours and minutes
124+
expect(position1).toBe(position2);
125+
});
126+
127+
it("should calculate position proportionally within an hour", () => {
128+
// Verify that 3:26pm is approximately 43% between 3pm and 4pm
129+
const date3pm = new Date();
130+
date3pm.setHours(15, 0, 0, 0);
131+
const position3pm = getNowLinePosition(date3pm);
132+
133+
const date4pm = new Date();
134+
date4pm.setHours(16, 0, 0, 0);
135+
const position4pm = getNowLinePosition(date4pm);
136+
137+
const date326pm = new Date();
138+
date326pm.setHours(15, 26, 0, 0);
139+
const position326pm = getNowLinePosition(date326pm);
140+
141+
const hourHeight = position4pm - position3pm; // Should be 80px (4 slots * 20px)
142+
const offsetFrom3pm = position326pm - position3pm;
143+
const percentage = offsetFrom3pm / hourHeight;
144+
145+
// 26 minutes / 60 minutes = 0.4333... ≈ 43%
146+
expect(percentage).toBeCloseTo(26 / 60, 2);
147+
expect(percentage).toBeCloseTo(0.4333, 2);
148+
});
149+
});
150+
});

packages/web/src/views/Day/util/agenda/agenda.util.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,10 @@ export const getAgendaEventPosition = (date: Date) => {
2020
const slot = hours * 4 + Math.floor(minutes / MINUTES_PER_SLOT);
2121
return slot * SLOT_HEIGHT;
2222
};
23+
24+
// Get exact position in pixels for the now line (accounts for fractional minutes)
25+
export const getNowLinePosition = (date: Date) => {
26+
const hours = date.getHours();
27+
const minutes = date.getMinutes();
28+
return (hours * 4 + minutes / MINUTES_PER_SLOT) * SLOT_HEIGHT;
29+
};

0 commit comments

Comments
 (0)