Skip to content

Commit a92a9df

Browse files
feat: Add Start/Stop Workout button and timer display
This commit introduces a new `WorkoutControls` component to the main dashboard, allowing users to start and stop a workout session. The new component includes: - 'Start Workout' and 'Stop Workout' buttons that send commands to the server via WebSocket. - A running timer that displays the elapsed workout time in MM:SS format, managed within the component's state and synchronized with the server's state. The `WorkoutControls` component is integrated into the main dashboard page (`app/page.tsx`), making it the primary control for workout sessions.
1 parent b19bd13 commit a92a9df

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

app/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import ErrorBoundary from '../components/ErrorBoundary'
1313
import ErrorFallback from '../components/ErrorFallback'
1414
import HrmTiles from '../components/HrmTiles'
1515
import TimerDisplay from '../components/TimerDisplay'
16+
import WorkoutControls from '../components/WorkoutControls'
1617
import { useWebSocket } from '@/context/WebSocketContext'
1718

1819
const DOC_URL =
@@ -54,6 +55,11 @@ const Dashboard = () => {
5455
}}
5556
>
5657
<Grid container spacing={{ xs: 2, sm: 2, md: 3 }}>
58+
{/* --------------------- WORKOUT CONTROLS --------------------- */}
59+
<Grid item xs={12}>
60+
<WorkoutControls />
61+
</Grid>
62+
5763
{/* --------------------- TOP ROW: TIMER + HR TILES --------------------- */}
5864

5965
{/* 1. TABATA TIMER - Componentized */}

components/WorkoutControls.tsx

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// File: components/WorkoutControls.tsx
2+
'use client'
3+
import { useWebSocket } from '@/context/WebSocketContext'
4+
import {
5+
TimerCommandMessage,
6+
} from '@/types/websocket'
7+
import PlayArrow from '@mui/icons-material/PlayArrow'
8+
import Stop from '@mui/icons-material/Stop'
9+
import Box from '@mui/material/Box'
10+
import Button from '@mui/material/Button'
11+
import Card from '@mui/material/Card'
12+
import CardContent from '@mui/material/CardContent'
13+
import Stack from '@mui/material/Stack'
14+
import Typography from '@mui/material/Typography'
15+
import { useCallback, useEffect, useState } from 'react'
16+
17+
const actionButtonSx = {
18+
flex: 1,
19+
fontWeight: 'bold',
20+
py: 1.5,
21+
minHeight: '64px',
22+
transition: 'transform 0.1s ease-in-out',
23+
'&:active': {
24+
transform: 'scale(0.95)',
25+
},
26+
}
27+
28+
const WorkoutControls = () => {
29+
const { timerData, sendData, isConnected } = useWebSocket()
30+
const [elapsedTime, setElapsedTime] = useState(0)
31+
32+
useEffect(() => {
33+
let interval: NodeJS.Timeout
34+
if (timerData.isRunning) {
35+
// Initialize elapsedTime based on the data from WebSocket
36+
setElapsedTime(timerData.timeElapsed)
37+
interval = setInterval(() => {
38+
setElapsedTime((prevTime) => prevTime + 1)
39+
}, 1000)
40+
} else {
41+
setElapsedTime(0)
42+
}
43+
return () => clearInterval(interval)
44+
}, [timerData.isRunning, timerData.timeElapsed])
45+
46+
const formatTime = (totalSeconds: number) => {
47+
const minutes = Math.floor(totalSeconds / 60)
48+
const seconds = totalSeconds % 60
49+
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(
50+
2,
51+
'0'
52+
)}`
53+
}
54+
55+
const sendTimerCommand = useCallback(
56+
(command: 'START' | 'STOP') => {
57+
const message: TimerCommandMessage = { type: 'TIMER_COMMAND', command }
58+
sendData(message)
59+
},
60+
[sendData]
61+
)
62+
63+
return (
64+
<Card
65+
sx={{
66+
boxShadow: 3,
67+
mb: 2,
68+
}}
69+
>
70+
<CardContent>
71+
<Stack direction="row" spacing={2} alignItems="center">
72+
<Box sx={{ flexGrow: 1 }}>
73+
<Typography variant="h6">
74+
Workout Timer
75+
</Typography>
76+
<Typography variant="h4" color="text.primary" sx={{ fontFamily: 'monospace' }}>
77+
{formatTime(elapsedTime)}
78+
</Typography>
79+
</Box>
80+
<Stack direction="row" spacing={2}>
81+
{!timerData.isRunning ? (
82+
<Button
83+
variant="contained"
84+
color="success"
85+
onClick={() => sendTimerCommand('START')}
86+
sx={actionButtonSx}
87+
startIcon={<PlayArrow />}
88+
disabled={!isConnected}
89+
>
90+
Start Workout
91+
</Button>
92+
) : (
93+
<Button
94+
variant="contained"
95+
color="error"
96+
onClick={() => sendTimerCommand('STOP')}
97+
sx={actionButtonSx}
98+
startIcon={<Stop />}
99+
disabled={!isConnected}
100+
>
101+
Stop Workout
102+
</Button>
103+
)}
104+
</Stack>
105+
</Stack>
106+
</CardContent>
107+
</Card>
108+
)
109+
}
110+
111+
export default WorkoutControls

0 commit comments

Comments
 (0)