Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store workout data in indexed db #364

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"@svgr/webpack": "^8.1.0",
"babel-plugin-named-asset-import": "^0.3.8",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"dexie": "^3.2.5",
"dexie-react-hooks": "^1.1.7",
"dotenv": "^16.3.1",
"dotenv-expand": "^10.0.0",
"eslint": "^8.52.0",
Expand Down
31 changes: 20 additions & 11 deletions apps/frontend/src/components/Graphs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,29 @@ export const Graphs = ({
showOtherUsersData,
activeGroupSession,
}: Props) => {
const { data: laps, untrackedData: rawUntrackedData } = useData();
const rawData = laps.flatMap((x) => x.dataPoints);
const { graphData: rawData } = useData();
const numPoints = 500;

const allMerged = React.useMemo(() => {
const data = rawData.map((dataPoint) => ({
'You HR': dataPoint.heartRate,
'You Power': dataPoint.power,
}));
const data = rawData.map((dataPoint) => {
if (dataPoint.tracking) {
return {
'You HR': dataPoint.heartRate,
'You Power': dataPoint.power,
};
}
return {};
});

const untrackedData = rawUntrackedData.map((dataPoint) => ({
'You-Untracked HR': dataPoint.heartRate,
'You-Untracked Power': dataPoint.power,
}));
const untrackedData = rawData.map((dataPoint) => {
if (!dataPoint.tracking) {
return {
'You-Untracked HR': dataPoint.heartRate,
'You-Untracked Power': dataPoint.power,
};
}
return {};
});

const otherPeoplesDataMerged = otherUsers
.map((username) => {
Expand Down Expand Up @@ -93,7 +102,7 @@ export const Graphs = ({
mergeArrays(filledData, otherPeoplesDataMerged),
filledUntrackedData
);
}, [rawData, rawUntrackedData, otherUsers, activeGroupSession]);
}, [rawData, otherUsers, activeGroupSession]);

const myAvgPower = Math.floor(
[...rawData].splice(-3).reduce((sum, data) => sum + (data.power || 0), 0) /
Expand Down
12 changes: 10 additions & 2 deletions apps/frontend/src/components/MainActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { WorkoutControls } from './MainActionBar/WorkoutControls';
import { SelectWorkoutButton } from './MainActionBar/SelectWorkoutButton';
import { useLinkColor } from '../hooks/useLinkColor';
import { Icon } from '@chakra-ui/react';
import { RecoverWorkout } from './MainActionBar/RecoverWorkout';

export const MainActionBar = () => {
const [showPowerControls, setShowPowerControls] = React.useState(false);
Expand All @@ -28,7 +29,14 @@ export const MainActionBar = () => {
const bgColor = useColorModeValue('gray.200', 'gray.900');
return (
<Center mb="5" mx="2">
<Stack p="5" borderRadius="1em" bgColor={bgColor} pointerEvents="auto">
<Stack
p="5"
borderRadius="1em"
bgColor={bgColor}
pointerEvents="auto"
width="500px"
>
<RecoverWorkout />
{showWorkoutControls ? <WorkoutControls /> : null}
{showPowerControls ? <PowerControls /> : null}

Expand Down Expand Up @@ -59,7 +67,7 @@ export const MainActionBar = () => {
</Tooltip>
</HStack>
</Center>
<Center width="8em">
<Center>
<StartButton />
</Center>
<Center height="100%">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button } from '@chakra-ui/button';
import { Download } from 'react-bootstrap-icons';
import { toTCX } from '../../createTcxFile';
import { downloadTCX } from '../../createTcxFile';
import { useData } from '../../context/DataContext';
import { Icon } from '@chakra-ui/react';

Expand All @@ -9,11 +9,11 @@ export const DownloadTCXButton = ({
}: {
includeGPSData: boolean;
}) => {
const { data, distance } = useData();
const { trackedData, distance } = useData();
return (
<Button
width="100%"
onClick={() => toTCX(data, distance, includeGPSData)}
onClick={() => /* TODO: Fix this downloadTCX(data, distance, includeGPSData) */ {}}
leftIcon={<Icon as={Download} />}
>
Save TCX
Expand Down
47 changes: 47 additions & 0 deletions apps/frontend/src/components/MainActionBar/RecoverWorkout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Button, Center, Grid, Stack, Text } from '@chakra-ui/react';
import { useWorkoutState } from '../../hooks/useWorkoutState';
import { useData } from '../../context/DataContext';
import { relativeHours } from '@dundring/utils';
import { startNewWorkout } from '../../db';

export const RecoverWorkout = () => {
const {
firstDatapoint,
lastDatapoint,
showRecoverPrompt,
setShowRecoverPrompt,
} = useWorkoutState();

const { state } = useData();

if (!firstDatapoint || !lastDatapoint) return null;

if (state !== 'not_started') return null;

if (!showRecoverPrompt) return null;

const timeAgo = relativeHours(
Math.floor(Date.now() - firstDatapoint.timestamp.getTime())
);

return (
<Grid templateColumns="2fr 1fr" gap="2">
<Stack>
<Text fontWeight="bold">Recent workout data found!</Text>
<Text>
We found workout data from a workout you started {timeAgo}. Do you
want to continue or start a new workout?
</Text>
</Stack>

<Center>
<Stack>
<Button onClick={() => setShowRecoverPrompt(false)}>
Continue workout
</Button>
<Button onClick={() => startNewWorkout()}>Start new workout</Button>
</Stack>
</Center>
</Grid>
);
};
18 changes: 6 additions & 12 deletions apps/frontend/src/components/MainActionBar/WorkoutControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import {
} from 'react-bootstrap-icons';
import { useNavigate } from 'react-router-dom';
import { useActiveWorkout } from '../../context/ActiveWorkoutContext';
import { useData } from '../../context/DataContext';
import { useWorkoutEditorModal } from '../../context/ModalContext';
import { useLinkColor } from '../../hooks/useLinkColor';
import { ActiveWorkout } from '../../types';

Expand All @@ -42,7 +40,6 @@ const getPlayButtonText = (activeWorkout: ActiveWorkout) => {
export const WorkoutControls = () => {
const { activeWorkout, syncResistance, changeActivePart, pause, start } =
useActiveWorkout();
const { addLap } = useData();
const linkColor = useLinkColor();
const navigate = useNavigate();

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

if (activeWorkout.status === 'finished') {
changeActivePart(
activeWorkout.workout.parts.length - 1,
addLap
);
changeActivePart(activeWorkout.workout.parts.length - 1);
return;
}
if (activeWorkoutPart <= 0) return;

changeActivePart(activeWorkoutPart - 1, addLap);
changeActivePart(activeWorkoutPart - 1);
}}
/>
</Tooltip>
Expand All @@ -108,7 +102,7 @@ export const WorkoutControls = () => {
onClick={() => {
if (!activeWorkout.workout) return;

changeActivePart(activeWorkoutPart, addLap);
changeActivePart(activeWorkoutPart);
}}
/>
</Tooltip>
Expand All @@ -130,7 +124,7 @@ export const WorkoutControls = () => {

switch (activeWorkout.status) {
case 'finished': {
changeActivePart(0, addLap);
changeActivePart(0);
return;
}

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

if (activeWorkout.status === 'finished') {
changeActivePart(0, addLap);
changeActivePart(0);
} else {
changeActivePart(activeWorkoutPart + 1, addLap);
changeActivePart(activeWorkoutPart + 1);
}
}}
/>
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/components/Map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { Waypoint } from '../../types';
import { powerColor } from '../../colors';
import { useColorModeValue } from '@chakra-ui/react';
import { toWebMercatorCoordinates } from '../../gps';
import { useActiveRoute } from '../../hooks/useActiveRoute';

export const Map = () => {
const { data: laps, activeRoute } = useData();
const { trackedData: rawData } = useData();
const { activeRoute } = useActiveRoute();
const dotColor = useColorModeValue('black', 'white');
const routeColor = useColorModeValue('#bdbdbd', '#424242');
const rawData = laps.flatMap((x) => x.dataPoints);

const multiplier = 40;

Expand Down
4 changes: 1 addition & 3 deletions apps/frontend/src/components/WorkoutDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Text, Stack } from '@chakra-ui/layout';
import * as React from 'react';
import { useActiveWorkout } from '../context/ActiveWorkoutContext';
import { useData } from '../context/DataContext';
import { Workout } from '../types';
import { wattFromFtpPercent } from '../utils/general';
import {
Expand All @@ -11,7 +10,6 @@ import {

export const WorkoutDisplay = () => {
const { activeWorkout, activeFtp, changeActivePart } = useActiveWorkout();
const { addLap } = useData();
if (!activeWorkout.workout) {
return null;
}
Expand All @@ -31,7 +29,7 @@ export const WorkoutDisplay = () => {
fontWeight={isActive ? 'bold' : 'normal'}
color={isActive ? 'purple.500' : ''}
cursor="pointer"
onClick={() => changeActivePart(i, addLap)}
onClick={() => changeActivePart(i)}
>
{`${secondsToHoursMinutesAndSecondsString(part.duration)}@${
part.targetPower
Expand Down
4 changes: 2 additions & 2 deletions apps/frontend/src/components/WorkoutMenu/WorkoutOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import { WorkoutToEdit } from '../Modals/WorkoutEditorModal';
import { defaultWorkouts } from './defaultWorkouts';
import { ImportWorkout } from './ImportWorkout';
import { WorkoutListItem } from './WorkoutListItem';
import { useData } from '../../context/DataContext';
import { stringToRouteName } from '../../gps';
import { useActiveRoute } from '../../hooks/useActiveRoute';

interface Props {
setActiveWorkout: (workout: Workout, ftp: number) => void;
Expand All @@ -32,7 +32,7 @@ export const WorkoutOverview = ({
const { activeFtp, setActiveFtp } = useActiveWorkout();
const [previewFtp, setPreviewFtp] = React.useState('' + activeFtp);
const previewFtpAsNumber = parseInputAsInt(previewFtp);
const { activeRoute, setActiveRoute } = useData();
const { activeRoute, setActiveRoute } = useActiveRoute();

const allUserWorkouts = [
...workouts.map((workout) => ({
Expand Down
18 changes: 7 additions & 11 deletions apps/frontend/src/context/ActiveWorkoutContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { useUser } from './UserContext';
export const ActiveWorkoutContext = React.createContext<{
activeWorkout: ActiveWorkout;
setActiveWorkout: (workout: Workout) => void;
increaseElapsedTime: (millis: number, addLap: () => void) => void;
increaseElapsedTime: (millis: number) => void;
start: () => void;
pause: () => void;
activeFtp: number;
setActiveFtp: (ftp: number) => void;
changeActivePart: (partNumber: number, addLap: () => void) => void;
changeActivePart: (partNumber: number) => void;
syncResistance: () => void;
syncResistanceIfActive: () => void;
} | null>(null);
Expand All @@ -22,7 +22,6 @@ interface IncreasePartElapsedTimeAction {
millis: number;
setResistance: (resistance: number) => void;
activeFtp: number;
addLap: () => void;
}

interface SetWorkoutAction {
Expand All @@ -45,7 +44,6 @@ interface ChangeActivePartAction {
type: 'CHANGE_ACTIVE_PART';
setResistance: (resistance: number) => void;
activeFtp: number;
addLap: () => void;
partNumber: number;
}

Expand Down Expand Up @@ -102,7 +100,7 @@ export const ActiveWorkoutContextProvider = ({
activePart: 0,
status: 'finished',
};
action.addLap();
// TODO: action.addLap();
return nextState;
}
// Only done with current part, other parts unfinished
Expand All @@ -111,7 +109,7 @@ export const ActiveWorkoutContextProvider = ({
partElapsedTime: newElapsed - currentPartDuration * 1000,
activePart: activeWorkout.activePart + 1,
};
action.addLap();
//TODO: action.addLap();
return nextState;
}
// Current part is not finished
Expand All @@ -127,7 +125,7 @@ export const ActiveWorkoutContextProvider = ({
case 'CHANGE_ACTIVE_PART': {
if (!activeWorkout.workout) return activeWorkout;

action.addLap();
// TODO: action.addLap();

if (action.partNumber >= activeWorkout.workout.parts.length) {
return { ...activeWorkout, partElapsedTime: 0, status: 'finished' };
Expand Down Expand Up @@ -218,23 +216,21 @@ export const ActiveWorkoutContextProvider = ({
setResistance(targetPowerAsWatt);
};

const changeActivePart = (partNumber: number, addLap: () => void) => {
const changeActivePart = (partNumber: number) => {
dispatchActiveWorkoutAction({
type: 'CHANGE_ACTIVE_PART',
setResistance,
activeFtp,
addLap,
partNumber,
});
};

const increaseElapsedTime = (millis: number, addLap: () => void) => {
const increaseElapsedTime = (millis: number) => {
dispatchActiveWorkoutAction({
type: 'INCREASE_PART_ELAPSED_TIME',
millis,
setResistance,
activeFtp,
addLap,
});
};

Expand Down
Loading