Skip to content

Commit 6807d60

Browse files
committed
Show duration based remote progress
1 parent 915347b commit 6807d60

3 files changed

Lines changed: 144 additions & 33 deletions

File tree

src/components/NotifyCompRemoteBar/NotifyCompRemoteBar.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { Container } from '@/components/Container';
33
import { useCompetitionRemoteControl } from '@/hooks/useCompetitionRemoteControl';
44
import { useNow } from '@/hooks/useNow';
55
import {
6-
formatElapsedMMSS,
6+
formatElapsedDuration,
77
formatNextActivityOffset,
8-
getActiveGroupStartTime,
98
getRemoteBarProgress,
109
} from './remoteBarProgress';
1110

@@ -32,13 +31,17 @@ export function NotifyCompRemoteBar({ competitionId }: NotifyCompRemoteBarProps)
3231
const activeNames = remote.activeGroups.map((group) => group.name);
3332
const currentTitle = activeNames.length > 0 ? activeNames.join(', ') : 'No active activity';
3433
const nextTitle = remote.nextGroup?.name || 'No next activity';
35-
const elapsed = formatElapsedMMSS(getActiveGroupStartTime(remote.activeGroups), now);
34+
const elapsed = formatElapsedDuration(remote.activeGroups, now);
3635
const nextOffset = formatNextActivityOffset(remote.nextGroup, now);
3736
const progress = getRemoteBarProgress({
3837
activeGroups: remote.activeGroups,
39-
nextGroup: remote.nextGroup,
4038
now,
4139
});
40+
const progressBarClassName = classNames('block h-full rounded-full', {
41+
'bg-blue-500 dark:bg-blue-400': progress.tone === 'normal',
42+
'bg-yellow-400 dark:bg-yellow-300': progress.tone === 'warning',
43+
'bg-red-500 dark:bg-red-400': progress.tone === 'overdue',
44+
});
4245

4346
const runSwitch = (direction: 'previous' | 'next') => {
4447
const group = direction === 'previous' ? remote.previousGroup : remote.nextGroup;
@@ -126,10 +129,7 @@ export function NotifyCompRemoteBar({ competitionId }: NotifyCompRemoteBarProps)
126129

127130
<div className="flex h-4 items-center">
128131
<div className="h-1 w-full overflow-hidden rounded-full bg-tertiary">
129-
<span
130-
className="block h-full rounded-full bg-blue-500 dark:bg-blue-400"
131-
style={{ width: `${progress}%` }}
132-
/>
132+
<span className={progressBarClassName} style={{ width: `${progress.percent}%` }} />
133133
</div>
134134
</div>
135135
</div>

src/components/NotifyCompRemoteBar/remoteBarProgress.test.ts

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { RemoteActivityGroup } from '@/lib/notifyCompRemoteActivities';
22
import {
3+
formatElapsedDuration,
34
formatElapsedMMSS,
45
formatNextActivityOffset,
56
getRemoteBarProgress,
@@ -35,6 +36,26 @@ describe('remoteBarProgress', () => {
3536
);
3637
});
3738

39+
it('formats elapsed time against scheduled activity duration', () => {
40+
expect(
41+
formatElapsedDuration(
42+
[
43+
group({
44+
liveActivities: [
45+
{
46+
activityId: 101,
47+
endTime: null,
48+
startTime: '2026-06-01T10:01:05Z',
49+
},
50+
],
51+
status: 'current',
52+
}),
53+
],
54+
new Date('2026-06-01T10:06:09Z'),
55+
),
56+
).toBe('05:04 / 10 minutes');
57+
});
58+
3859
it('describes how many minutes remain until the next activity', () => {
3960
expect(
4061
formatNextActivityOffset(
@@ -68,7 +89,7 @@ describe('remoteBarProgress', () => {
6889
).toBe('In 4 hours');
6990
});
7091

71-
it('fills progress based on live start time and next scheduled start time', () => {
92+
it('fills progress based on live start time and scheduled activity duration', () => {
7293
expect(
7394
getRemoteBarProgress({
7495
activeGroups: [
@@ -83,19 +104,57 @@ describe('remoteBarProgress', () => {
83104
status: 'current',
84105
}),
85106
],
86-
nextGroup: group({
87-
id: '222-r1-g1',
88-
name: '2x2x2 Cube, Round 1, Group 1',
89-
scheduledActivities: [
90-
{
91-
...group({}).scheduledActivities[0],
92-
id: 102,
93-
startTime: '2026-06-01T10:20:00Z',
94-
},
95-
],
96-
}),
97107
now: new Date('2026-06-01T10:05:00Z'),
98108
}),
99-
).toBe(25);
109+
).toEqual({
110+
percent: 50,
111+
tone: 'normal',
112+
});
113+
});
114+
115+
it('uses a warning tone once the activity is within 5 percent of done', () => {
116+
expect(
117+
getRemoteBarProgress({
118+
activeGroups: [
119+
group({
120+
liveActivities: [
121+
{
122+
activityId: 101,
123+
endTime: null,
124+
startTime: '2026-06-01T10:00:00Z',
125+
},
126+
],
127+
status: 'current',
128+
}),
129+
],
130+
now: new Date('2026-06-01T10:09:30Z'),
131+
}),
132+
).toEqual({
133+
percent: 95,
134+
tone: 'warning',
135+
});
136+
});
137+
138+
it('uses an overdue tone once the scheduled duration has elapsed', () => {
139+
expect(
140+
getRemoteBarProgress({
141+
activeGroups: [
142+
group({
143+
liveActivities: [
144+
{
145+
activityId: 101,
146+
endTime: null,
147+
startTime: '2026-06-01T10:00:00Z',
148+
},
149+
],
150+
status: 'current',
151+
}),
152+
],
153+
now: new Date('2026-06-01T10:11:00Z'),
154+
}),
155+
).toEqual({
156+
percent: 100,
157+
tone: 'overdue',
158+
});
100159
});
101160
});

src/components/NotifyCompRemoteBar/remoteBarProgress.ts

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ const getEarliestTime = (times: Array<number | undefined>) => {
1919
return validTimes.length ? Math.min(...validTimes) : undefined;
2020
};
2121

22+
const getLatestTime = (times: Array<number | undefined>) => {
23+
const validTimes = times.filter((time): time is number => time !== undefined);
24+
return validTimes.length ? Math.max(...validTimes) : undefined;
25+
};
26+
2227
export const formatElapsedMMSS = (startTime: number | string | undefined, now: Date) => {
2328
const start = typeof startTime === 'number' ? startTime : getTime(startTime);
2429

@@ -36,6 +41,20 @@ export const formatElapsedMMSS = (startTime: number | string | undefined, now: D
3641
export const getGroupScheduledStartTime = (group?: RemoteActivityGroup) =>
3742
getEarliestTime(group?.scheduledActivities.map((activity) => getTime(activity.startTime)) || []);
3843

44+
const getActiveGroupsScheduledStartTime = (groups: RemoteActivityGroup[]) =>
45+
getEarliestTime(
46+
groups.flatMap((group) =>
47+
group.scheduledActivities.map((activity) => getTime(activity.startTime)),
48+
),
49+
);
50+
51+
const getActiveGroupsScheduledEndTime = (groups: RemoteActivityGroup[]) =>
52+
getLatestTime(
53+
groups.flatMap((group) =>
54+
group.scheduledActivities.map((activity) => getTime(activity.endTime)),
55+
),
56+
);
57+
3958
export const getActiveGroupStartTime = (groups: RemoteActivityGroup[]) =>
4059
getEarliestTime(
4160
groups.flatMap((group) => {
@@ -51,6 +70,33 @@ export const getActiveGroupStartTime = (groups: RemoteActivityGroup[]) =>
5170
}),
5271
);
5372

73+
export const getActiveGroupsScheduledDuration = (groups: RemoteActivityGroup[]) => {
74+
const start = getActiveGroupsScheduledStartTime(groups);
75+
const end = getActiveGroupsScheduledEndTime(groups);
76+
77+
if (start === undefined || end === undefined || end <= start) {
78+
return undefined;
79+
}
80+
81+
return end - start;
82+
};
83+
84+
const formatDurationMinutes = (duration: number | undefined) => {
85+
if (duration === undefined) {
86+
return '0 minutes';
87+
}
88+
89+
const minutes = Math.max(1, Math.round(duration / MINUTE));
90+
const unit = minutes === 1 ? 'minute' : 'minutes';
91+
92+
return `${minutes} ${unit}`;
93+
};
94+
95+
export const formatElapsedDuration = (groups: RemoteActivityGroup[], now: Date) =>
96+
`${formatElapsedMMSS(getActiveGroupStartTime(groups), now)} / ${formatDurationMinutes(
97+
getActiveGroupsScheduledDuration(groups),
98+
)}`;
99+
54100
export const formatNextActivityOffset = (nextGroup: RemoteActivityGroup | undefined, now: Date) => {
55101
const nextStart = getGroupScheduledStartTime(nextGroup);
56102

@@ -79,25 +125,31 @@ export const formatNextActivityOffset = (nextGroup: RemoteActivityGroup | undefi
79125

80126
export const getRemoteBarProgress = ({
81127
activeGroups,
82-
nextGroup,
83128
now,
84129
}: {
85130
activeGroups: RemoteActivityGroup[];
86-
nextGroup?: RemoteActivityGroup;
87131
now: Date;
88132
}) => {
89133
const activeStart = getActiveGroupStartTime(activeGroups);
90-
const nextStart = getGroupScheduledStartTime(nextGroup);
91-
92-
if (activeStart === undefined || nextStart === undefined) {
93-
return nextStart !== undefined && now.getTime() >= nextStart ? 100 : 0;
94-
}
95-
96-
const duration = nextStart - activeStart;
134+
const duration = getActiveGroupsScheduledDuration(activeGroups);
97135

98-
if (duration <= 0) {
99-
return now.getTime() >= nextStart ? 100 : 0;
136+
if (activeStart === undefined || duration === undefined) {
137+
return {
138+
percent: 0,
139+
tone: 'normal' as const,
140+
};
100141
}
101142

102-
return clampPercent(((now.getTime() - activeStart) / duration) * 100);
143+
const elapsed = Math.max(0, now.getTime() - activeStart);
144+
const percent = (elapsed / duration) * 100;
145+
146+
return {
147+
percent: clampPercent(percent),
148+
tone:
149+
percent > 100
150+
? ('overdue' as const)
151+
: percent >= 95
152+
? ('warning' as const)
153+
: ('normal' as const),
154+
};
103155
};

0 commit comments

Comments
 (0)