Browse Source

adds cooldown, fixed stats bug and fixes middle layout

master
Stian Aune 5 years ago
parent
commit
9dbd74967c
  1. 29
      my-bois/package-lock.json
  2. 1
      my-bois/package.json
  3. 37
      my-bois/src/components/Bois.jsx
  4. 45
      my-bois/src/components/Contexts.jsx
  5. 2
      my-bois/src/components/Misc.css
  6. 9
      my-bois/src/components/Misc.jsx
  7. 8
      my-bois/src/components/Score.css
  8. 19
      my-bois/src/components/Score.jsx
  9. 27
      my-bois/src/helpers/color.test.js
  10. 49
      my-bois/src/helpers/diff.js
  11. 89
      my-bois/src/helpers/diff.test.js
  12. 8
      my-bois/src/hooks/milestones.js
  13. 14
      my-bois/src/hooks/net.js
  14. 23
      my-bois/src/hooks/options.js

29
my-bois/package-lock.json

@ -1288,6 +1288,16 @@
"loader-utils": "^1.2.3" "loader-utils": "^1.2.3"
} }
}, },
"@testing-library/react-hooks": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-3.2.1.tgz",
"integrity": "sha512-1OB6Ksvlk6BCJA1xpj8/WWz0XVd1qRcgqdaFAq+xeC6l61Ucj0P6QpA5u+Db/x9gU4DCX8ziR5b66Mlfg0M2RA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.5.4",
"@types/testing-library__react-hooks": "^3.0.0"
}
},
"@types/babel__core": { "@types/babel__core": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.3.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.3.tgz",
@ -1378,11 +1388,30 @@
"csstype": "^2.2.0" "csstype": "^2.2.0"
} }
}, },
"@types/react-test-renderer": {
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.1.tgz",
"integrity": "sha512-nCXQokZN1jp+QkoDNmDZwoWpKY8HDczqevIDO4Uv9/s9rbGPbSpy8Uaxa5ixHKkcm/Wt0Y9C3wCxZivh4Al+rQ==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/stack-utils": { "@types/stack-utils": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==" "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw=="
}, },
"@types/testing-library__react-hooks": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.1.0.tgz",
"integrity": "sha512-QJc1sgH9DD6jbfybzugnP0sY8wPzzIq8sHDBuThzCr2ZEbyHIaAvN9ytx/tHzcWL5MqmeZJqiUm/GsythaGx3g==",
"dev": true,
"requires": {
"@types/react": "*",
"@types/react-test-renderer": "*"
}
},
"@types/yargs": { "@types/yargs": {
"version": "13.0.3", "version": "13.0.3",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.3.tgz",

1
my-bois/package.json

@ -60,6 +60,7 @@
"workbox-webpack-plugin": "4.3.1" "workbox-webpack-plugin": "4.3.1"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/react-hooks": "^3.2.1",
"@types/react": "^16.9.11" "@types/react": "^16.9.11"
}, },
"scripts": { "scripts": {

37
my-bois/src/components/Bois.jsx

@ -2,11 +2,11 @@ import React, {useContext, useEffect, useState} from 'react';
import "./Bois.css"; import "./Bois.css";
import {StatusContext} from "./Contexts"; import {StatusContext} from "./Contexts";
import {CalorieScore, CpmScore, DistanceScore, RpmScore, Timer} from "./Score";
import {CalorieScore, CpmScore, LevelScore, RpmScore, Timer} from "./Score";
import calculateDiff from "../helpers/diff"; import calculateDiff from "../helpers/diff";
import useKey from "../hooks/useKey"; import useKey from "../hooks/useKey";
import {Milestones} from "./Milestones"; import {Milestones} from "./Milestones";
import {Info, InfoTable, StateFilter, Warning} from "./Misc";
import {Info, InfoTable, StateFilter, Differ} from "./Misc";
const Boi = ({type, children}) => { const Boi = ({type, children}) => {
return ( return (
@ -17,22 +17,23 @@ const Boi = ({type, children}) => {
}; };
export const LeftBoi = () => { export const LeftBoi = () => {
const {prevLongDiff, workoutStatus, program, hidden} = useContext(StatusContext);
if (workoutStatus === null || program === null || hidden) {
const {prevLongDiff, workout, workoutStatus, program, hidden} = useContext(StatusContext);
if (workoutStatus === null || workout === null || program === null || hidden) {
return null; return null;
} }
const {minutes, seconds, calories, distance, rpm} = workoutStatus;
const diff = calculateDiff(program, minutes, seconds, calories);
const {cooldownMin} = workout;
const {minutes, seconds, calories, level, rpm} = workoutStatus;
const diff = calculateDiff({program, cooldownMin, minutes, seconds, calories});
const cpm = calories / (minutes + (seconds / 60)); const cpm = calories / (minutes + (seconds / 60));
return ( return (
<Boi type="left"> <Boi type="left">
<Timer minutes={minutes} seconds={seconds}/>
<Timer minutes={minutes} seconds={seconds} cooldownMin={cooldownMin}/>
<CalorieScore calories={calories} diff={diff} prevDiff={prevLongDiff}/> <CalorieScore calories={calories} diff={diff} prevDiff={prevLongDiff}/>
<DistanceScore distance={distance}/>
<RpmScore rpm={rpm}/> <RpmScore rpm={rpm}/>
<CpmScore cpm={cpm}/> <CpmScore cpm={cpm}/>
<LevelScore level={level}/>
</Boi> </Boi>
); );
}; };
@ -42,6 +43,9 @@ export const CentreBoi = () => {
state, program, setProgram, bike, setBike, bikes, programs, state, program, setProgram, bike, setBike, bikes, programs,
start, pause, stop, create, workoutStatus, start, pause, stop, create, workoutStatus,
hidden, setHidden, hidden, setHidden,
workout,
prevLongDiff,
toggleCooldown,
} = useContext(StatusContext); } = useContext(StatusContext);
const [options, setOptions] = useState(null); const [options, setOptions] = useState(null);
const [current, setCurrent] = useState(0); const [current, setCurrent] = useState(0);
@ -115,6 +119,12 @@ export const CentreBoi = () => {
useKey(["H", "h"], () => showHide()); useKey(["H", "h"], () => showHide());
useKey("*", () => {
if (state === "started") {
toggleCooldown();
}
});
function showHide() { function showHide() {
setHidden(!hidden); setHidden(!hidden);
} }
@ -124,13 +134,14 @@ export const CentreBoi = () => {
} }
if (state === "started" && workoutStatus !== null) { if (state === "started" && workoutStatus !== null) {
const {cooldownMin} = workout;
const {minutes, seconds, calories} = workoutStatus; const {minutes, seconds, calories} = workoutStatus;
const diff = calculateDiff(program, minutes, seconds, calories);
const diff = calculateDiff({program, cooldownMin, minutes, seconds, calories});
if (diff < 0) {
if (diff < 0 || (minutes > 0 && seconds === 0)) {
return ( return (
<Boi type="centre"> <Boi type="centre">
<Warning>{diff}</Warning>
<Differ diff={diff} prevDiff={prevLongDiff} />
</Boi> </Boi>
); );
} else { } else {
@ -147,15 +158,13 @@ export const CentreBoi = () => {
return ""; return "";
} }
if (options[current] === void(0)) {
if (options[current] === void (0)) {
return ""; return "";
} }
return options[current].name; return options[current].name;
} }
console.log(state);
return ( return (
<Boi type="centre"> <Boi type="centre">
<StateFilter current={state} required="stopped"> <StateFilter current={state} required="stopped">

45
my-bois/src/components/Contexts.jsx

@ -1,13 +1,15 @@
import React, {createContext, useEffect, useState} from "react";
import React, {createContext, useCallback, useEffect, useState} from "react";
import { import {
connectWorkout, connectWorkout,
createNewWorkout, createNewWorkout,
fetchActiveWorkouts, fetchBikes, fetchPrograms,
openWebsocket, pauseWorkout,
fetchActiveWorkouts,
openWebsocket,
pauseWorkout,
startWorkout, startWorkout,
stopWorkout
stopWorkout, updateCooldownMins
} from "../hooks/net"; } from "../hooks/net";
import useMilestones from "../hooks/milestones"; import useMilestones from "../hooks/milestones";
import useOptions from "../hooks/options";
export const StatusContext = createContext({ export const StatusContext = createContext({
bike: null, bike: null,
@ -28,10 +30,9 @@ export const StatusContext = createContext({
}); });
export const StatusContextProvider = ({children}) => { export const StatusContextProvider = ({children}) => {
const {bikes, programs} = useOptions();
const [bike, setBike] = useState(null); const [bike, setBike] = useState(null);
const [program, setProgram] = useState(null); const [program, setProgram] = useState(null);
const [bikes, setBikes] = useState(null);
const [programs, setPrograms] = useState(null);
const [workout, setWorkout] = useState(null); const [workout, setWorkout] = useState(null);
const [workoutStatus, setWorkoutStatus] = useState(null); const [workoutStatus, setWorkoutStatus] = useState(null);
const [state, setState] = useState("offline"); const [state, setState] = useState("offline");
@ -39,21 +40,24 @@ export const StatusContextProvider = ({children}) => {
const [socket, setSocket] = useState(null); const [socket, setSocket] = useState(null);
const [hidden, setHidden] = useState(false); const [hidden, setHidden] = useState(false);
useEffect(() => {
if (programs === null) {
fetchPrograms().then(newPrograms => setPrograms(newPrograms));
const toggleCooldown = useCallback(async () => {
if (workout === null || workoutStatus === null) {
return;
} }
}, [programs]);
useEffect(() => {
if (bikes === null) {
fetchBikes().then(newBikes => {
setBikes(newBikes);
const {cooldownMin} = workout;
const {minutes} = workoutStatus;
if (newBikes.length === 1) {
setBike(newBikes[0]);
if (cooldownMin === -1) {
setWorkout(await updateCooldownMins(workout, minutes + 1));
} else if (minutes < cooldownMin) {
setWorkout(await updateCooldownMins(workout, -1));
} }
});
}, [workout, workoutStatus]);
useEffect(() => {
if (bikes !== null && bikes.length === 1) {
setBike(bikes[0]);
} }
}, [bikes]); }, [bikes]);
@ -94,15 +98,17 @@ export const StatusContextProvider = ({children}) => {
} }
if (typeof body.workoutStatusBackfill !== "undefined") { if (typeof body.workoutStatusBackfill !== "undefined") {
const cooldownMin = workout.cooldownMin;
body.workoutStatusBackfill.forEach(wsbf => { body.workoutStatusBackfill.forEach(wsbf => {
setWorkoutStatus(wsbf); setWorkoutStatus(wsbf);
msDispatch({type: "measure", payload: {...wsbf, program}});
msDispatch({type: "measure", payload: {...wsbf, program, cooldownMin}});
}); });
} }
if (typeof body.workoutStatus !== "undefined") { if (typeof body.workoutStatus !== "undefined") {
const cooldownMin = workout.cooldownMin;
setWorkoutStatus(body.workoutStatus); setWorkoutStatus(body.workoutStatus);
msDispatch({type: "measure", payload: {...body.workoutStatus, program}});
msDispatch({type: "measure", payload: {...body.workoutStatus, program, cooldownMin}});
} }
}; };
@ -162,6 +168,7 @@ export const StatusContextProvider = ({children}) => {
programs, bikes, programs, bikes,
prevDiff, prevLongDiff, prevDiff, prevLongDiff,
hidden, setHidden, hidden, setHidden,
toggleCooldown,
}}> }}>
{children} {children}
</StatusContext.Provider> </StatusContext.Provider>

2
my-bois/src/components/Misc.css

@ -1,5 +1,5 @@
.Warning { .Warning {
margin-top: 1em;
margin-top: 0;
font-weight: 800; font-weight: 800;
font-size: 150%; font-size: 150%;
} }

9
my-bois/src/components/Misc.jsx

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import {COLOR_VERY_BAD} from "../helpers/color";
import {COLOR_VERY_BAD, colorByDiff} from "../helpers/color";
import "./Misc.css"; import "./Misc.css";
import {diffString} from "../helpers/diff";
const Filter = ({bool, children}) => bool ? <>{children}</> : null; const Filter = ({bool, children}) => bool ? <>{children}</> : null;
@ -9,9 +10,9 @@ export const StateFilter = ({current, required, children}) => (
<Filter bool={current === required}>{children}</Filter> <Filter bool={current === required}>{children}</Filter>
); );
export const Warning = ({children}) => (
<div className="Warning" style={{color: COLOR_VERY_BAD}}>
{children}
export const Differ = ({diff, prevDiff}) => (
<div className="Warning" style={{color: colorByDiff(diff, prevDiff)}}>
{diffString(diff)}
</div> </div>
); );

8
my-bois/src/components/Score.css

@ -10,3 +10,11 @@
font-size: 125%; font-size: 125%;
font-weight: 800; font-weight: 800;
} }
.Timer-cooldown {
color: #0BF;
}
.Timer-awaits-cooldown {
color: #8FF;
}

19
my-bois/src/components/Score.jsx

@ -5,7 +5,7 @@ import "./Score.css";
const Score = ({value, suffix = null, color = COLOR_NEUTRAL}) => ( const Score = ({value, suffix = null, color = COLOR_NEUTRAL}) => (
<div className="Score"> <div className="Score">
<span className="Score-value" style={{color}}>{value}</span>
<span className="Score-value" style={{color}}>{!isNaN(value) ? value : 0}</span>
{suffix !== null && ( {suffix !== null && (
<> <>
&nbsp; &nbsp;
@ -15,13 +15,24 @@ const Score = ({value, suffix = null, color = COLOR_NEUTRAL}) => (
</div> </div>
); );
export const Timer = ({minutes, seconds}) => {
export const Timer = ({minutes, seconds, cooldownMin}) => {
function pad(number) { function pad(number) {
return number >= 10 ? `${number}` : `0${number}`; return number >= 10 ? `${number}` : `0${number}`;
} }
const hasCooldown = cooldownMin >= 0 && cooldownMin !== void(0);
const isCooldown = hasCooldown && minutes >= cooldownMin;
const awaitsCooldown = hasCooldown && !isCooldown;
const classes = ["Timer"];
if (isCooldown) {
classes.push("Timer-cooldown");
} else if (awaitsCooldown) {
classes.push("Timer-awaits-cooldown");
}
return ( return (
<div className="Timer">
<div className={classes.join(" ")}>
<span className="Timer-number">{pad(minutes)}</span> <span className="Timer-number">{pad(minutes)}</span>
{" : "} {" : "}
<span className="Timer-number">{pad(seconds)}</span> <span className="Timer-number">{pad(seconds)}</span>
@ -33,7 +44,7 @@ export const CalorieScore = ({calories, diff, prevDiff}) => (
<Score value={calories} color={colorByDiff(diff, prevDiff)} suffix="kal"/> <Score value={calories} color={colorByDiff(diff, prevDiff)} suffix="kal"/>
); );
export const DistanceScore = ({distance}) => <Score value={distance.toFixed(1)} suffix="km"/>;
export const LevelScore = ({level}) => <Score value={level} suffix="lvl"/>;
export const RpmScore = ({rpm}) => <Score value={rpm} suffix="rpm"/>; export const RpmScore = ({rpm}) => <Score value={rpm} suffix="rpm"/>;

27
my-bois/src/helpers/color.test.js

@ -0,0 +1,27 @@
import {COLOR_BAD, COLOR_GOOD, COLOR_VERY_BAD, COLOR_VERY_GOOD, colorByDiff} from "./color";
describe("colorByDiff", function () {
it("should show dark red for worse bad", () => {
expect(colorByDiff(-5, -2)).toBe(COLOR_VERY_BAD);
});
it("should show bright red for equal bad", () => {
expect(colorByDiff(-11, -11)).toBe(COLOR_BAD);
});
it("should show bright red for better bad", () => {
expect(colorByDiff(-1, -7)).toBe(COLOR_BAD);
});
it("should show dark green for better good", () => {
expect(colorByDiff(33, 25)).toBe(COLOR_VERY_GOOD);
});
it("should show bright green for equal good", () => {
expect(colorByDiff(10, 10)).toBe(COLOR_GOOD);
});
it("should show bright green for worse good", () => {
expect(colorByDiff(5, 7)).toBe(COLOR_GOOD);
});
});

49
my-bois/src/helpers/diff.js

@ -2,24 +2,39 @@ export function diffString(diff) {
return diff < 0 ? `${diff}` : `+${diff}` return diff < 0 ? `${diff}` : `+${diff}`
} }
export default function calculateDiff(program, minutes, seconds, calories) {
const {warmupMin, warmupCpm, cpm} = program;
let preWarmup = 0;
if (warmupMin > 0) {
// Pre-warmup
const warmedUpMinutes = Math.min(minutes, warmupMin);
const warmedUpSeconds = minutes >= warmupMin ? 0 : seconds;
preWarmup = Math.round((warmupCpm * (warmedUpMinutes + (warmedUpSeconds / 60))));
}
export default function calculateDiff({program, cooldownMin = null, minutes, seconds, calories}) {
const {warmupMin, warmupCpm, cpm, cooldownCpm} = program;
const actualWarmup = cooldownMin !== null && cooldownMin > 0
? Math.min(warmupMin, cooldownMin)
: warmupMin;
// Minutes in each section
const minWarmup = calculateMins(0, actualWarmup, minutes, seconds);
const minMain = calculateMins(actualWarmup, cooldownMin, minutes, seconds);
const minCooldown = calculateMins(cooldownMin, null, minutes, seconds);
// Post-warmup
const trainedMinutes = Math.max(0, minutes - warmupMin);
const trainedSeconds = minutes >= warmupMin ? seconds : 0;
const postWarmup = Math.round((cpm * (trainedMinutes + (trainedSeconds / 60))));
// Expected calories in each section
const calWarmup = minWarmup * warmupCpm;
const calMain = minMain * cpm;
const calCooldown = minCooldown * cooldownCpm;
// Sum
const target = Math.round(preWarmup + postWarmup);
return Math.round(calories - (calWarmup + calMain + calCooldown));
}
return calories - target;
function calculateMins(minMinutes, maxMinutes, minutes, seconds) {
const fraction = seconds / 60;
if (minMinutes === null || minMinutes < 0) {
return 0.0;
}
if (minutes >= minMinutes) {
if (maxMinutes !== null && minutes >= maxMinutes) {
return maxMinutes - minMinutes;
}
return (minutes - minMinutes) + fraction;
} else {
return 0.0;
}
} }

89
my-bois/src/helpers/diff.test.js

@ -0,0 +1,89 @@
import calculateDiff, {diffString} from "./diff";
const programWithoutWarmup = {
warmupMin: 0,
warmupCpm: 0,
cpm: 30,
cooldownCpm: 20,
};
const programWithWarmup = {
warmupMin: 10,
warmupCpm: 25,
cpm: 30,
cooldownCpm: 20,
};
describe('diffString', () => {
it("should show a plus before zero", () => {
expect(diffString(0)).toBe("+0");
});
it("should show a plus before a positive number", () => {
expect(diffString(44)).toBe("+44");
});
it("should show a minus when negatives", () => {
expect(diffString(-12)).toBe("-12");
});
});
describe("calculateDiff", () => {
it("should return zero at start", () => {
const diff = calculateDiff({
program: programWithoutWarmup,
minutes: 0,
seconds: 0,
calories: 0,
});
expect(diff).toBe(0);
});
it("should expect 300 calories after 0' / 10'", () => {
const diff = calculateDiff({
program: programWithoutWarmup,
minutes: 10,
seconds: 0,
calories: 305,
});
expect(diff).toBe(5);
});
it("should expect 865 calories after 10' / 20'50", () => {
const diff = calculateDiff({
program: programWithWarmup,
minutes: 30,
seconds: 50,
calories: 250 + 600 + 25 - 17,
});
// Aim: 875
expect(diff).toBe(-17);
});
it("should expect 755 calories after 10' / 15' / 7'45", () => {
const diff = calculateDiff({
program: programWithWarmup,
cooldownMin: 25,
minutes: 32,
seconds: 45,
calories: 250 + 450 + 140 + 15,
});
expect(diff).toBe(0);
});
it("should expect 325 calories after 5' (half) / 0' / 10'", () => {
const diff = calculateDiff({
program: programWithWarmup,
cooldownMin: 5,
minutes: 15,
seconds: 0,
calories: 325
});
expect(diff).toBe(0);
});
});

8
my-bois/src/hooks/milestones.js

@ -11,19 +11,21 @@ function reducer(state, {type, payload}) {
switch (type) { switch (type) {
case "measure": case "measure":
let {prevDiff, prevLongDiff, milestones} = state; let {prevDiff, prevLongDiff, milestones} = state;
const {minutes, seconds, calories, program} = payload;
const {minutes, seconds, calories, program, cooldownMin} = payload;
if (minutes === 0 || seconds !== 0) { if (minutes === 0 || seconds !== 0) {
return state; return state;
} }
const isFive = minutes % 5 === 0; const isFive = minutes % 5 === 0;
const diff = calculateDiff(program, minutes, seconds, calories);
let newMilestones = milestones.filter(m => m.minutes !== minutes);
const diff = calculateDiff({program, minutes, seconds, calories, cooldownMin});
let newMilestones = [...milestones];
if (newMilestones.find(m => m.minutes === minutes) === void(0)) {
newMilestones.push({ newMilestones.push({
minutes, seconds, calories, diff, minutes, seconds, calories, diff,
prevDiff: isFive ? prevLongDiff : prevDiff prevDiff: isFive ? prevLongDiff : prevDiff
}); });
}
if (isFive) { if (isFive) {
newMilestones = newMilestones.filter(m => m.minutes % 5 === 0); newMilestones = newMilestones.filter(m => m.minutes % 5 === 0);

14
my-bois/src/hooks/net.js

@ -33,6 +33,10 @@ export async function pauseWorkout(workout) {
return await post(`/workout/${workout.id}/pause`); return await post(`/workout/${workout.id}/pause`);
} }
export async function updateCooldownMins(workout, cooldownMin) {
return await put(`/workout/${workout.id}`, {cooldownMin});
}
export function openWebsocket(workout) { export function openWebsocket(workout) {
return new WebSocket(url(`/workout/${workout.id}/subscribe`, "ws")); return new WebSocket(url(`/workout/${workout.id}/subscribe`, "ws"));
} }
@ -53,6 +57,16 @@ function post(path, data = {}) {
}).then(r => r.json()); }).then(r => r.json());
} }
function put(path, data = {}) {
return fetch(url(path), {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data)
}).then(r => r.json());
}
function url(path, prefix = "http") { function url(path, prefix = "http") {
return `${prefix}://127.0.0.1:9999/api${path}`; return `${prefix}://127.0.0.1:9999/api${path}`;
} }

23
my-bois/src/hooks/options.js

@ -0,0 +1,23 @@
import {useEffect, useState} from "react";
import {fetchBikes, fetchPrograms} from "./net";
export default function useOptions() {
const [bikes, setBikes] = useState(null);
const [programs, setPrograms] = useState(null);
useEffect(() => {
if (programs === null) {
fetchPrograms().then(newPrograms => setPrograms(newPrograms));
}
}, [programs]);
useEffect(() => {
if (bikes === null) {
fetchBikes().then(newBikes => {
setBikes(newBikes);
});
}
}, [bikes]);
return {bikes, programs};
}
Loading…
Cancel
Save