|
|
import "./ProgramBoi.sass";
import {useContext, useEffect, useMemo, useReducer} from "react"; import RuntimeContext from "../../contexts/RuntimeContext"; import {ProgramStep, weighting} from "../../models/Programs"; import {firstKey, WorkoutState} from "../../models/Workouts"; import {diffLinearValues, formatValue} from "../../models/Shared"; import {Boi} from "../../primitives/boi/Boi"; import {useLastMilestoneValue} from "../../hooks/milestones";
interface StepMeta { actualDuration?: WorkoutState }
interface ProgressState { steps: (ProgramStep & StepMeta)[] currentIndex: number lastTransition: WorkoutState lastValue: WorkoutState toNext: ToNext stopped: boolean }
interface ToNext { current: number, max: number, }
interface ProgressChange { skip?: boolean workoutState?: WorkoutState }
function calculateToNext( step: ProgramStep & StepMeta, lastValue: WorkoutState, lastTransition: WorkoutState, ): ToNext { if (step.duration) { if (step.duration.time) { return { current: lastValue.time - lastTransition.time, max: step.duration.time, }; } else if (step.duration.calories && lastTransition.calories !== undefined && lastValue.calories !== undefined) { return { current: lastValue.calories - lastTransition.calories, max: step.duration.calories, }; } else if (step.duration.distance && lastTransition.distance !== undefined && lastValue.distance !== undefined) { return { current: lastValue.distance - lastTransition.distance, max: step.duration.distance, }; } }
return { current: 0, max: 1, } }
function programReducer(state: ProgressState, change: ProgressChange) { let {steps, currentIndex, lastTransition, lastValue, toNext, stopped} = state;
// Stop working if after program
if (stopped) { return state; } else if (currentIndex > steps.length - 1) { return {...state, stopped: true}; }
// Skip
if (change.skip) { steps[currentIndex].actualDuration = diffLinearValues(lastValue, lastTransition);
return { ...state, steps, lastTransition: lastValue, currentIndex: currentIndex + 1, }; }
// Workout state
if (change.workoutState) { lastValue = change.workoutState; const step = steps[currentIndex];
if (step.duration) { toNext = calculateToNext(step, lastValue, lastTransition);
if (toNext.current >= toNext.max) { steps[currentIndex].actualDuration = diffLinearValues(lastValue, lastTransition); currentIndex += 1; lastTransition = lastValue;
if (steps.length > currentIndex) { toNext = calculateToNext(steps[currentIndex], lastValue, lastTransition); } } } }
return {steps, currentIndex, lastTransition, lastValue, toNext, stopped}; }
export default function ProgramBoi() { const {workout, lastEvent} = useContext(RuntimeContext); const program = workout!.program!; const [progress, dispatch] = useReducer(programReducer, { steps: program.steps, currentIndex: 0, lastTransition: {time: 0, distance: 0, calories: 0}, lastValue: {time: 0}, toNext: {current: 0, max: 1}, stopped: false, });
useEffect(() => { if (lastEvent) { for (const workoutState of lastEvent?.workoutStates || []) { dispatch({workoutState}); }
if (lastEvent?.event?.name === "Skipped") { dispatch({skip: true}); } } }, [lastEvent]);
if (progress.steps.some(s => weighting(s) === 0)) { return <InfiniteProgress progress={progress}/>; }
return <HealthBarProgress progress={progress}/>; }
interface ProgressProps { progress: ProgressState }
type FromTo = { count: false } | { count: true rawCurr: number rawTo: number strCurr: string strTo: string }
function InfiniteProgress({progress}: ProgressProps) { const offset = 6 - progress.steps.length + progress.currentIndex; const ft: FromTo = useMemo(() => { const currentStep = progress.steps[progress.currentIndex]; if (currentStep) { const key = firstKey(currentStep.duration as WorkoutState); if (key !== null) { const duration = progress.steps[progress.currentIndex].duration![key]!; const numStr = formatValue(progress.toNext.current, key); const toStr = formatValue(duration, key);
return { count: true, rawCurr: progress.toNext.current, rawTo: duration, strCurr: numStr, strTo: toStr, } as FromTo } }
return {count: false}; }, [progress]);
if (progress.currentIndex >= progress.steps.length) { return null; }
return ( <Boi vertical="bottom" horizontal="center" unchunky style={{fontSize: "2vmax", minWidth: "24vmax"}} > <div style={{padding: "0.5vmax 0.5vmax 0", fontWeight: 400}}> Steg {progress.currentIndex + 1} / {progress.steps.length} </div> {ft.count && ( <div> <div style={{margin: "0.5vmax"}}> {ft.strCurr} / {ft.strTo} </div> <div className="ProgressBar"> <div className={`ProgressBar-level-${offset}`} style={{flex: ft.rawCurr}}/> <div className={`ProgressBar-bg`} style={{flex: ft.rawTo - ft.rawCurr}}/> </div> </div> )} {!ft.count && ( <div style={{margin: "0.5vmax"}}> (Custom) </div> )} </Boi> ); }
function HealthBarProgress({progress}: ProgressProps) { const offset = 6 - progress.steps.length; const steps = progress.steps.reduce((acc, b) => ([b, ...acc]), [] as typeof progress.steps);
return ( <div className="HealthBar"> {steps.map((step, i) => <HealthBarProgressStep key={i} progress={progress} step={step} offset={offset}/>)} </div> ); }
interface HealthBarProgressStepProps { progress: ProgressState step: ProgramStep & StepMeta offset: number }
function HealthBarProgressStep({progress, step, offset}: HealthBarProgressStepProps) { const stepIndex = progress.steps.indexOf(step); const level = progress.steps.indexOf(step) + offset;
const max = Math.max(1, progress.toNext.max); const key = firstKey(step.duration as WorkoutState); const lastMilestone = useLastMilestoneValue(key!, "time");
const duration = step.duration![key!]!; const durationStr = formatValue(duration - progress.toNext.current, key!);
const currentVal = Math.max(0, progress.toNext.current);
const preDiffed = key !== "time" ? Math.max(0, (progress.lastValue[key!] || 0) - lastMilestone) : -1; const diffed = Math.min(preDiffed, currentVal);
return ( <div key={step.index} className="HealthBar-entry" style={{flex: weighting(step)}}> {stepIndex === progress.currentIndex && ( <div className="HealthBar-entry-text"> {durationStr} </div> )} <div className="ProgressBar"> {stepIndex > progress.currentIndex && ( <div className={`ProgressBar-inactive`} style={{flex: 1}}/> )} {stepIndex === progress.currentIndex && ( <> <div className={`ProgressBar-level-${level}`} style={{flex: max - currentVal}}/> {diffed >= 0 && ( <div className={`ProgressBar-level-${level} diff`} style={{flex: diffed}}/> )} <div className={`ProgressBar-bg`} style={{flex: currentVal - diffed}}/> </> )} {stepIndex < progress.currentIndex && ( <div className={`ProgressBar-bg`} style={{flex: 1}}/> )} </div> </div> ); }
|