Skip to content

Commit cb99339

Browse files
authored
Merge pull request #478 from sivertschou/dexie
Store workout data with IndexedDB
2 parents 5dc1ea9 + ed58f62 commit cb99339

22 files changed

+645
-325
lines changed

apps/frontend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111
},
1212
"dependencies": {
1313
"@chakra-ui/react": "^2.8.1",
14+
"@dundring/utils": "*",
1415
"@emotion/react": "^11.13.3",
1516
"@emotion/styled": "^11.14.0",
1617
"@rgrove/parse-xml": "^4.1.0",
1718
"@svgr/webpack": "^8.1.0",
1819
"babel-plugin-named-asset-import": "^0.3.8",
1920
"case-sensitive-paths-webpack-plugin": "^2.4.0",
21+
"dexie": "^4.0.10",
22+
"dexie-react-hooks": "^1.1.7",
2023
"dotenv": "^16.3.1",
2124
"eslint": "^8.52.0",
2225
"eslint-config-react-app": "^7.0.1",
@@ -44,7 +47,6 @@
4447
},
4548
"devDependencies": {
4649
"@dundring/types": "*",
47-
"@dundring/utils": "*",
4850
"@types/dom-screen-wake-lock": "^1.0.2",
4951
"@types/react": "^18.2.34",
5052
"@types/react-beautiful-dnd": "^13.1.6",

apps/frontend/src/components/Graphs.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,29 @@ export const Graphs = ({
3232
showOtherUsersData,
3333
activeGroupSession,
3434
}: Props) => {
35-
const { data: laps, untrackedData: rawUntrackedData } = useData();
36-
const rawData = laps.flatMap((x) => x.dataPoints);
35+
const { graphData: rawData } = useData();
3736
const numPoints = 500;
3837

3938
const allMerged = React.useMemo(() => {
40-
const data = rawData.map((dataPoint) => ({
41-
'You HR': dataPoint.heartRate,
42-
'You Power': dataPoint.power,
43-
}));
39+
const data = rawData.map((dataPoint) => {
40+
if (dataPoint.tracking) {
41+
return {
42+
'You HR': dataPoint.heartRate,
43+
'You Power': dataPoint.power,
44+
};
45+
}
46+
return {};
47+
});
4448

45-
const untrackedData = rawUntrackedData.map((dataPoint) => ({
46-
'You-Untracked HR': dataPoint.heartRate,
47-
'You-Untracked Power': dataPoint.power,
48-
}));
49+
const untrackedData = rawData.map((dataPoint) => {
50+
if (!dataPoint.tracking) {
51+
return {
52+
'You-Untracked HR': dataPoint.heartRate,
53+
'You-Untracked Power': dataPoint.power,
54+
};
55+
}
56+
return {};
57+
});
4958

5059
const otherPeoplesDataMerged = otherUsers
5160
.map((username) => {
@@ -93,7 +102,7 @@ export const Graphs = ({
93102
mergeArrays(filledData, otherPeoplesDataMerged),
94103
filledUntrackedData
95104
);
96-
}, [rawData, rawUntrackedData, otherUsers, activeGroupSession]);
105+
}, [rawData, otherUsers, activeGroupSession]);
97106

98107
const myAvgPower = Math.floor(
99108
[...rawData].splice(-3).reduce((sum, data) => sum + (data.power || 0), 0) /

apps/frontend/src/components/MainActionBar.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { WorkoutControls } from './MainActionBar/WorkoutControls';
1313
import { SelectWorkoutButton } from './MainActionBar/SelectWorkoutButton';
1414
import { useLinkColor } from '../hooks/useLinkColor';
1515
import { Icon } from '@chakra-ui/react';
16+
import { RecoverWorkout } from './MainActionBar/RecoverWorkout';
1617

1718
export const MainActionBar = () => {
1819
const [showPowerControls, setShowPowerControls] = React.useState(false);
@@ -28,7 +29,14 @@ export const MainActionBar = () => {
2829
const bgColor = useColorModeValue('gray.200', 'gray.900');
2930
return (
3031
<Center mb="5" mx="2">
31-
<Stack p="5" borderRadius="1em" bgColor={bgColor} pointerEvents="auto">
32+
<Stack
33+
p="5"
34+
borderRadius="1em"
35+
bgColor={bgColor}
36+
pointerEvents="auto"
37+
width="500px"
38+
>
39+
<RecoverWorkout />
3240
<PausedWorkoutButtons />
3341
{showWorkoutControls ? <WorkoutControls /> : null}
3442
{showPowerControls ? <PowerControls /> : null}
@@ -60,7 +68,7 @@ export const MainActionBar = () => {
6068
</Tooltip>
6169
</HStack>
6270
</Center>
63-
<Center width="8em">
71+
<Center>
6472
<StartButton />
6573
</Center>
6674
<Center height="100%">

apps/frontend/src/components/MainActionBar/DownloadTCXButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import { Icon } from '@chakra-ui/react';
55
import { downloadTcx } from '../../createTcxFile';
66

77
export const DownloadTCXButton = ({}: {}) => {
8-
const { data, distance } = useData();
8+
const { trackedData } = useData();
99
return (
1010
<Button
1111
width="100%"
12-
onClick={() => downloadTcx(data, distance)}
12+
onClick={() => downloadTcx(trackedData)}
1313
leftIcon={<Icon as={Download} />}
1414
>
1515
Download TCX
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Button, Center, Grid, Stack, Text } from '@chakra-ui/react';
2+
import { useWorkoutState } from '../../hooks/useWorkoutState';
3+
import { useData } from '../../context/DataContext';
4+
import { relativeHours } from '@dundring/utils';
5+
import { startNewWorkout } from '../../db';
6+
import { useState } from 'react';
7+
8+
export const RecoverWorkout = () => {
9+
const { firstDatapoint, lastDatapoint } = useWorkoutState();
10+
11+
const [showRecoverPrompt, setShowRecoverPrompt] = useState(true);
12+
const { state } = useData();
13+
14+
if (!firstDatapoint || !lastDatapoint) return null;
15+
16+
if (state !== 'not_started') return null;
17+
18+
if (!showRecoverPrompt) return null;
19+
20+
const timeAgo = relativeHours(
21+
Math.floor(Date.now() - firstDatapoint.timestamp.getTime())
22+
);
23+
24+
return (
25+
<Grid templateColumns="2fr 1fr" gap="2">
26+
<Stack>
27+
<Text fontWeight="bold">Recent workout data found!</Text>
28+
<Text>
29+
We found workout data from a workout you started {timeAgo}. Do you
30+
want to continue or start a new workout?
31+
</Text>
32+
</Stack>
33+
34+
<Center>
35+
<Stack>
36+
<Button onClick={() => setShowRecoverPrompt(false)}>
37+
Continue workout
38+
</Button>
39+
<Button onClick={() => startNewWorkout()}>Start new workout</Button>
40+
</Stack>
41+
</Center>
42+
</Grid>
43+
);
44+
};

apps/frontend/src/components/MainActionBar/UploadToStravaButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useState } from 'react';
1010
import { useActiveWorkout } from '../../context/ActiveWorkoutContext';
1111

1212
export const UploadToStravaButton = () => {
13-
const { data, distance } = useData();
13+
const { trackedData } = useData();
1414
const { user } = useUser();
1515
const { activeWorkout } = useActiveWorkout();
1616

@@ -52,7 +52,7 @@ export const UploadToStravaButton = () => {
5252
api
5353
.uploadActivity(
5454
user.token,
55-
toTcxString(data, distance),
55+
toTcxString(trackedData),
5656
activeWorkout.workout?.name ?? null
5757
)
5858
.then((response) => {

apps/frontend/src/components/MainActionBar/WorkoutControls.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import {
2020
} from 'react-bootstrap-icons';
2121
import { useNavigate } from 'react-router-dom';
2222
import { useActiveWorkout } from '../../context/ActiveWorkoutContext';
23-
import { useData } from '../../context/DataContext';
24-
import { useWorkoutEditorModal } from '../../context/ModalContext';
2523
import { useLinkColor } from '../../hooks/useLinkColor';
2624
import { ActiveWorkout } from '../../types';
2725

@@ -42,7 +40,6 @@ const getPlayButtonText = (activeWorkout: ActiveWorkout) => {
4240
export const WorkoutControls = () => {
4341
const { activeWorkout, syncResistance, changeActivePart, pause, start } =
4442
useActiveWorkout();
45-
const { addLap } = useData();
4643
const linkColor = useLinkColor();
4744
const navigate = useNavigate();
4845

@@ -86,15 +83,12 @@ export const WorkoutControls = () => {
8683
if (!activeWorkout.workout) return;
8784

8885
if (activeWorkout.status === 'finished') {
89-
changeActivePart(
90-
activeWorkout.workout.parts.length - 1,
91-
addLap
92-
);
86+
changeActivePart(activeWorkout.workout.parts.length - 1);
9387
return;
9488
}
9589
if (activeWorkoutPart <= 0) return;
9690

97-
changeActivePart(activeWorkoutPart - 1, addLap);
91+
changeActivePart(activeWorkoutPart - 1);
9892
}}
9993
/>
10094
</Tooltip>
@@ -108,7 +102,7 @@ export const WorkoutControls = () => {
108102
onClick={() => {
109103
if (!activeWorkout.workout) return;
110104

111-
changeActivePart(activeWorkoutPart, addLap);
105+
changeActivePart(activeWorkoutPart);
112106
}}
113107
/>
114108
</Tooltip>
@@ -130,7 +124,7 @@ export const WorkoutControls = () => {
130124

131125
switch (activeWorkout.status) {
132126
case 'finished': {
133-
changeActivePart(0, addLap);
127+
changeActivePart(0);
134128
return;
135129
}
136130

@@ -156,9 +150,9 @@ export const WorkoutControls = () => {
156150
if (!activeWorkout.workout) return;
157151

158152
if (activeWorkout.status === 'finished') {
159-
changeActivePart(0, addLap);
153+
changeActivePart(0);
160154
} else {
161-
changeActivePart(activeWorkoutPart + 1, addLap);
155+
changeActivePart(activeWorkoutPart + 1);
162156
}
163157
}}
164158
/>

apps/frontend/src/components/Map/Map.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { Waypoint } from '../../types';
44
import { powerColor } from '../../colors';
55
import { useColorModeValue } from '@chakra-ui/react';
66
import { toWebMercatorCoordinates } from '../../gps';
7+
import { useActiveRoute } from '../../hooks/useActiveRoute';
78

89
export const Map = () => {
9-
const { data: laps, activeRoute } = useData();
10+
const { activeRoute } = useActiveRoute();
11+
const { trackedData: rawData } = useData();
1012
const dotColor = useColorModeValue('black', 'white');
1113
const routeColor = useColorModeValue('#bdbdbd', '#424242');
12-
const rawData = laps.flatMap((x) => x.dataPoints);
1314

1415
const multiplier = 40;
1516

apps/frontend/src/components/TopBar.tsx

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import {
1010
useActiveWorkout,
1111
} from '../context/ActiveWorkoutContext';
1212
import { secondsToHoursMinutesAndSecondsString } from '@dundring/utils';
13-
import { Lap } from '../types';
14-
import React from 'react';
1513
import { useReadOnlyOptions } from '../context/OptionsContext';
14+
import { useWorkoutState } from '../hooks/useWorkoutState';
1615

1716
const mainFontSize = ['xl', '3xl', '7xl'];
1817
const unitFontSize = ['l', '2xl', '4xl'];
@@ -22,17 +21,11 @@ export const TopBar = () => {
2221
const { cadence, currentResistance } = useSmartTrainer();
2322
const { heartRate } = useHeartRateMonitor();
2423
const { activeWorkout } = useActiveWorkout();
25-
const {
26-
data: laps,
27-
timeElapsed,
28-
distance,
29-
speed,
30-
smoothedPower,
31-
maxHeartRate,
32-
} = useData();
3324

3425
const options = useReadOnlyOptions();
3526

27+
const { timeElapsed, distance, speed, smoothedPower, maxHeartRate } =
28+
useData();
3629
const remainingTime = getRemainingTime(activeWorkout);
3730

3831
const secondsElapsed = Math.floor(timeElapsed / 1000);
@@ -44,8 +37,6 @@ export const TopBar = () => {
4437

4538
const isFreeMode = !currentResistance;
4639

47-
const currentLap = laps.at(-1) || null;
48-
4940
const hasRemainingTime = remainingTime !== null;
5041

5142
return (
@@ -110,7 +101,7 @@ export const TopBar = () => {
110101
<Text fontSize={mainFontSize}>{smoothedPower || '0'}</Text>
111102
<Text fontSize={unitFontSize}>w</Text>
112103
</Center>
113-
{isFreeMode && <AvgWattText currentLap={currentLap} />}
104+
{isFreeMode && <AvgWattText />}
114105

115106
<Text fontSize={secondaryFontSize}>{cadence || '0'} rpm</Text>
116107
</Stack>
@@ -121,15 +112,22 @@ export const TopBar = () => {
121112
);
122113
};
123114

124-
const AvgWattText = (props: { currentLap: Lap | null }) => {
125-
const { currentLap } = props;
126-
if (!currentLap?.normalizedDuration) {
127-
return null;
128-
}
115+
const AvgWattText = () => {
116+
const { lapData } = useWorkoutState();
117+
118+
const nonZeroWattLapDatapoints = lapData.filter(
119+
(datapoint) => !!datapoint.power
120+
);
121+
122+
const sumWatt = nonZeroWattLapDatapoints.reduce(
123+
(totalWatt, datapoint) => totalWatt + (datapoint.power ?? 0),
124+
0
125+
);
126+
129127
return (
130128
<Text fontSize={secondaryFontSize}>
131129
Lap avg:
132-
{(currentLap.sumWatt / currentLap.normalizedDuration).toFixed(0)}W
130+
{(sumWatt / Math.max(nonZeroWattLapDatapoints.length, 1)).toFixed(0)}W
133131
</Text>
134132
);
135133
};

apps/frontend/src/components/WorkoutDisplay.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Text, Stack } from '@chakra-ui/layout';
22
import * as React from 'react';
33
import { useActiveWorkout } from '../context/ActiveWorkoutContext';
4-
import { useData } from '../context/DataContext';
54
import { Workout } from '../types';
65
import { wattFromFtpPercent } from '../utils/general';
76
import {
@@ -11,7 +10,6 @@ import {
1110

1211
export const WorkoutDisplay = () => {
1312
const { activeWorkout, activeFtp, changeActivePart } = useActiveWorkout();
14-
const { addLap } = useData();
1513
if (!activeWorkout.workout) {
1614
return null;
1715
}
@@ -36,7 +34,7 @@ export const WorkoutDisplay = () => {
3634
fontWeight={isActive ? 'bold' : 'normal'}
3735
color={isActive ? 'purple.500' : ''}
3836
cursor="pointer"
39-
onClick={() => changeActivePart(i, addLap)}
37+
onClick={() => changeActivePart(i)}
4038
>
4139
{`${secondsToHoursMinutesAndSecondsString(part.duration)}@${targetPowerText}`}
4240
</Text>

0 commit comments

Comments
 (0)