Skip to content

Instantly share code, notes, and snippets.

@jkhaui
Created May 23, 2023 02:41
Show Gist options
  • Save jkhaui/926ed3ff6fa6468f689dd9d6ccfb42aa to your computer and use it in GitHub Desktop.
Save jkhaui/926ed3ff6fa6468f689dd9d6ccfb42aa to your computer and use it in GitHub Desktop.
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import {
Container,
Button,
Divider,
Icon,
Hidden,
WeeksTabs,
FeaturedCard,
Box,
Loader,
MoreOptionsMenu,
DialogBox
} from '@loup/ui-components';
import { JwConfig, IconConfig, FeatureConfig } from 'App/config';
import { CopyConsts } from 'App/constants';
import oldTheme from 'App/theme/theme';
import HeaderV2 from 'common/components/HeaderV2';
import ProgramWorkoutList from 'common/components/ProgramWorkoutList';
import ProgramDetailsModal from 'common/components/ProgramDetailsModal';
import { ProgramUpNextCard } from 'common/components/Program/ProgramUpNextCard';
import ErrorFullScreen from 'common/components/ErrorFullScreen';
import ProgramComplete from './ProgramComplete';
import { DockedBar } from './ProgramSchedulerView.styles.js';
import { useFeedback, useGetProgramWorkoutsQuery } from 'common/hooks';
import { WorkoutHelper } from 'common/helpers';
import { JWPlayer } from '@loup/connected-components/containers/Video';
import { ROUTES_HELPER } from '@loup/connected-components/helpers';
import { transformProgramScheduler } from '@loup/connected-components/store/transforms/programs';
import { ProgramProgressBar } from 'common/components/Program/ProgramProgressBar/ProgramProgressBar';
import { ProgramOptionalInfoBox } from 'common/components/Program/ProgramOptionalInfoBox/ProgramOptionalInfoBox';
import useLocalStorageState from 'use-local-storage-state';
const {
program: progCopy,
toast,
program: {
viewDetail: viewDetailCopy,
leave: leaveCopy,
complete: completeCopy
}
} = CopyConsts;
const { error: errorCopy } = CopyConsts;
const {
settings: {
error: { icon: errorIcon }
}
} = IconConfig;
const QUERY_PARAMS_NAME = {
CAN_CHECK_WORKOUT: 'checkable'
};
const LAST_ATTEMPT = {
COMPLETE_PROGRAM: 'COMPLETE_PROGRAM',
LEAVE_PROGRAM: 'LEAVE_PROGRAM'
};
const OPTIONAL_INFO_BOX_LOCAL_STORAGE_KEY = 'centr.optional.info.display';
const ProgramScheduler = ({
userName,
featuredImage,
title,
tag,
selectedWeek,
weeks,
workouts,
onWorkoutCheck,
onWeekChange,
isFetching,
onStartWorkout,
reward,
clearErrors,
onLeaveProgram,
history,
programId,
onLockedWeek,
isFetchingWorkouts,
hasStarted,
error,
information,
trainers,
canCompleteProgram,
onCompleteProgram,
imageList,
isFetchingProgramComplete,
programCompleted,
onSkipWorkout,
isSkippingWorkout
}) => {
const [initialFetchComplete, setInitialFetchComplete] = useState(false);
const [lastAttempt, setLastAttempt] = useState(null);
const initialFetchRef = useRef(isFetching);
const { showSnackbar, showModal, hideModal } = useFeedback();
const {
programsOptionalWorkouts: isProgramsOptionalWorkouts
} = FeatureConfig;
const hasMultipleWeeks = weeks && weeks.length > 1;
const jwProps = {
playerId: 'defaultvideoplayer',
playerScript: JwConfig.coached,
preload: 'auto',
onComplete: hideModal,
shouldPlay: true,
playlist: WorkoutHelper.getVideoMediaSource(reward && reward.media)
};
const [showCompleteModal, setShowCompleteModal] = useState(false);
const HEADER_HEIGHT = 56;
const WEEK_METADATA_LABEL = 'Week';
const checkable = ROUTES_HELPER.getQueryStringObject(location.search)?.[
QUERY_PARAMS_NAME.CAN_CHECK_WORKOUT
];
const currentProgress = weeks?.reduce(
(accumulator, currentValue) =>
accumulator +
currentValue.completed +
(isProgramsOptionalWorkouts ? currentValue?.skipped : 0),
0
);
const [
lastWeekWithIncompleteProgressIndex,
setLastWeekWithIncompleteProgressIndex
] = useState(-1);
console.log(
'lastWeekWithIncompleteProgressIndex: ',
lastWeekWithIncompleteProgressIndex
);
useEffect(() => {
if (
!isProgramsOptionalWorkouts ||
weeks.length === 0 ||
workouts.length === 0
) {
return;
}
const hasJustStartedProgram = currentProgress === 0;
if (hasJustStartedProgram) {
setLastWeekWithIncompleteProgressIndex(0);
return;
}
const lastCompletedOrSkippedWorkoutIndex = workouts.findLastIndex(
workout =>
workout.completed === true ||
(isProgramsOptionalWorkouts ? workout?.skipped : true)
);
const doesUpNextWorkoutExistInCurrentWeek =
lastCompletedOrSkippedWorkoutIndex !== -1 &&
!!workouts[lastCompletedOrSkippedWorkoutIndex + 1];
let _lastWeekWithIncompleteProgressIndex = weeks.findLastIndex(
week =>
!week.locked &&
week.completed + (isProgramsOptionalWorkouts ? week?.skipped : 0) !==
week.total &&
(week.completed > 0 || week.skipped > 0)
);
if (_lastWeekWithIncompleteProgressIndex === -1) {
_lastWeekWithIncompleteProgressIndex =
weeks.findLastIndex(
week =>
week.completed || (isProgramsOptionalWorkouts ? week.skipped : true)
) + 1;
}
setLastWeekWithIncompleteProgressIndex(
_lastWeekWithIncompleteProgressIndex
);
if (!doesUpNextWorkoutExistInCurrentWeek) {
// 2 paths from here: the user just completed the last workout of a week and all future weeks are empty, OR
// the user is clicking around on random weeks with future weeks filled.
const futureWeeks = weeks.slice(selectedWeek + 1);
const doFutureWeeksWithCompletedOrSkippedWorkoutsExist = futureWeeks.some(
week => !week.locked && (week.completed || week.skipped)
);
const haveAllWorkoutsInWeekBeenCompletedOrSkipped = workouts.every(
workout => workout.completed || workout.skipped
);
const lastWorkoutOfWeekHasBeenCompletedOrSkipped =
workouts.findLastIndex(
workout => workout.completed || workout.skipped
) +
1 ===
workouts.length;
if (doFutureWeeksWithCompletedOrSkippedWorkoutsExist) {
setLastWeekWithIncompleteProgressIndex(
_lastWeekWithIncompleteProgressIndex
);
return;
}
if (!doFutureWeeksWithCompletedOrSkippedWorkoutsExist) {
setLastWeekWithIncompleteProgressIndex(x => x + 1);
if (
(haveAllWorkoutsInWeekBeenCompletedOrSkipped ||
lastWorkoutOfWeekHasBeenCompletedOrSkipped) &&
lastWeekWithIncompleteProgressIndex === selectedWeek
) {
setLastWeekWithIncompleteProgressIndex(x => x + 1);
return;
}
}
// if (lastCompletedOrSkippedWorkoutIndex + 1 === workouts.length) {
}
}, [JSON.stringify(weeks), JSON.stringify(workouts)]);
const currentWeekText = `${WEEK_METADATA_LABEL} ${weeks[lastWeekWithIncompleteProgressIndex]?.text}`;
const { refetch } = useGetProgramWorkoutsQuery(
programId,
lastWeekWithIncompleteProgressIndex
);
const [upNextWorkout, setUpNextWorkout] = useState();
const totalWorkouts = weeks?.reduce(
(accumulator, currentValue) => accumulator + currentValue.total,
0
);
const shouldRefetchRef = useRef(false);
useEffect(() => {
if (
lastWeekWithIncompleteProgressIndex === -1 ||
!isProgramsOptionalWorkouts ||
weeks.length === 0 ||
workouts.length === 0
) {
return;
}
if (lastWeekWithIncompleteProgressIndex !== selectedWeek) {
refetch().then(({ data: res }) => {
const data = {
result: {
...res
}
};
const inProgressWeekWorkouts = transformProgramScheduler(data, {
level: 1
})?.workouts;
const lastCompletedOrSkippedWorkoutIndex = inProgressWeekWorkouts.findLastIndex(
workout =>
workout.completed === true ||
(isProgramsOptionalWorkouts ? workout?.skipped : true)
);
// If there's no complete or skipped workouts in this week, default to the first workout.
if (lastCompletedOrSkippedWorkoutIndex === -1) {
setUpNextWorkout(inProgressWeekWorkouts[0]);
return;
} else {
setUpNextWorkout(
inProgressWeekWorkouts[lastCompletedOrSkippedWorkoutIndex + 1]
);
return;
}
shouldRefetchRef.current = true;
});
return;
}
let upNextWorkoutIndex = workouts.findLastIndex(
workout =>
workout.completed === true ||
(isProgramsOptionalWorkouts ? workout?.skipped : true)
);
if (upNextWorkoutIndex !== -1) {
upNextWorkoutIndex = upNextWorkoutIndex + 1;
setUpNextWorkout(workouts[upNextWorkoutIndex]);
}
}, [
JSON.stringify(weeks),
JSON.stringify(workouts),
lastWeekWithIncompleteProgressIndex
]);
const [shouldDisplay, setShouldDisplay] = useLocalStorageState(
OPTIONAL_INFO_BOX_LOCAL_STORAGE_KEY,
{
defaultValue: true
}
);
const handleCompleteProgram = () => {
setLastAttempt(LAST_ATTEMPT.COMPLETE_PROGRAM);
setShowCompleteModal(true);
onCompleteProgram();
};
const handleLeaveProgram = () => {
setLastAttempt(LAST_ATTEMPT.LEAVE_PROGRAM);
onLeaveProgram();
};
const handleRetry = () => {
switch (lastAttempt) {
case LAST_ATTEMPT.COMPLETE_PROGRAM:
handleCompleteProgram();
break;
case LAST_ATTEMPT.LEAVE_PROGRAM:
handleLeaveProgram();
break;
default:
history.go(0);
}
};
const menuOptions = [
{
text: viewDetailCopy,
onClick: () => showModal(<ProgramDetailsModal programId={programId} />)
},
{
text: leaveCopy,
onClick: () =>
showModal(
<DialogBox
iconName={'exit'}
title={progCopy.leaveTitle}
description={progCopy.leaveDescription(title)}
leftButton={progCopy.leaveCancelButton}
rightButton={progCopy.leaveOkButton}
onLeftButtonClick={hideModal}
onRightButtonClick={() => {
hideModal();
handleLeaveProgram();
}}
/>,
{ showClose: false }
)
},
{
text: completeCopy,
onClick: handleCompleteProgram
}
];
useEffect(() => {
if (initialFetchRef.current && !isFetching) {
setInitialFetchComplete(true);
} else initialFetchRef.current = true;
if (error && error.toast) {
showSnackbar(toast.genericError);
clearErrors();
}
});
useEffect(() => {
if (!hasStarted && !isFetching) {
history.replace(`/program/${programId}${location.search}`);
}
}, [hasStarted, isFetching]);
const showUpNextWorkoutCard =
FeatureConfig.programsLandingProgress &&
workouts &&
!canCompleteProgram &&
upNextWorkout;
return (
<>
<HeaderV2 isFixed={!showCompleteModal}>
{!showCompleteModal && (
<MoreOptionsMenu
color={oldTheme.color('header-foreground')}
items={menuOptions}
/>
)}
</HeaderV2>
{error.status ? (
<Box
width={'100%'}
display="flex"
justifyContent={'center'}
alignItems={'center'}
height={'100vh'}
top={56}
>
<ErrorFullScreen
maxWidth={328}
icon={errorIcon}
heading={errorCopy.title}
description={errorCopy.message}
btnText={errorCopy.cta}
handleReset={handleRetry}
/>
</Box>
) : showCompleteModal ? (
<ProgramComplete
title={title}
subTitle={trainers?.join(' & ')}
imageList={imageList}
userName={userName}
offsetTop={HEADER_HEIGHT}
isLoading={
isFetchingProgramComplete ||
(!isFetchingProgramComplete && !programCompleted)
}
onClick={() => history.push('/programs')}
/>
) : (
<>
<Container
mt={HEADER_HEIGHT / 8}
px={0}
pb={canCompleteProgram ? 8 : 5}
position={'relative'}
minHeight={'75vh'}
>
{!initialFetchComplete && <Loader position={'absolute'} center />}
{title && (
<Hidden smDown>
<Container px={0}>
<FeaturedCard
title={title}
header={tag}
image={featuredImage}
/>
</Container>
</Hidden>
)}
{showUpNextWorkoutCard && (
<>
<ProgramProgressBar
currentProgress={currentProgress}
totalWorkouts={totalWorkouts}
title={title}
/>
<ProgramUpNextCard
weeks={weeks}
onStartWorkout={onStartWorkout}
currentWeekText={currentWeekText}
onSkipWorkout={onSkipWorkout}
programId={programId}
isSkippingWorkout={isSkippingWorkout}
{...upNextWorkout}
/>
<ProgramOptionalInfoBox
setShouldDisplay={setShouldDisplay}
shouldDisplay={upNextWorkout?.isSkippable && shouldDisplay}
/>
</>
)}
{weeks && weeks.length > 0 && (
<Box pb={2} pt={hasMultipleWeeks ? 2 : 0}>
<WeeksTabs
value={selectedWeek}
onTabChange={newIndex => onWeekChange(newIndex)}
onDisabledTabClick={onLockedWeek}
data={weeks?.map(week => {
const {
locked,
community,
total,
completed,
skipped = 0,
text
} = week;
return {
text,
disabled: locked,
content: (
<ProgramWorkoutList
workouts={workouts}
checkable={checkable}
onWorkoutCheck={onWorkoutCheck}
onStartWorkout={onStartWorkout}
isFetching={isFetchingWorkouts || isSkippingWorkout}
onWeekChange={onWeekChange}
reward={reward}
onRewardVideoClick={() =>
showModal(<JWPlayer {...jwProps} />, {
bgColor: 'common.black'
})
}
programId={programId}
onSkipWorkout={onSkipWorkout}
information={information}
/>
),
indicator: community,
percent:
total > 0
? Math.round(
((isProgramsOptionalWorkouts
? completed + skipped
: completed) /
total) *
100
)
: 0
};
})}
/>
</Box>
)}
</Container>
{canCompleteProgram && (
<DockedBar>
<Divider />
<Box py={1} px={3}>
<Box maxWidth={840} mx="auto">
<Button
color="primary"
py={2}
fullWidth
onClick={handleCompleteProgram}
disabled={isFetchingWorkouts || isFetchingProgramComplete}
>
<Icon name="star" mr={1} />
Complete program
</Button>
</Box>
</Box>
</DockedBar>
)}
</>
)}
</>
);
};
ProgramScheduler.propTypes = {
userName: PropTypes.string,
featuredImage: PropTypes.string,
title: PropTypes.string,
tag: PropTypes.string,
selectedWeek: PropTypes.number,
weeks: PropTypes.array,
workouts: PropTypes.object,
onWorkoutCheck: PropTypes.func,
onWeekChange: PropTypes.func,
onLockedWeek: PropTypes.func,
isFetching: PropTypes.bool,
programId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
onStartWorkout: PropTypes.func,
reward: PropTypes.object,
error: PropTypes.object,
clearErrors: PropTypes.func,
onLeaveProgram: PropTypes.func,
history: PropTypes.object,
isFetchingWorkouts: PropTypes.bool,
hasStarted: PropTypes.bool,
information: PropTypes.string,
canCompleteProgram: PropTypes.bool,
trainers: PropTypes.arrayOf(PropTypes.string),
onCompleteProgram: PropTypes.func,
imageList: PropTypes.object,
isFetchingProgramComplete: PropTypes.bool,
programCompleted: PropTypes.bool,
onSkipWorkout: PropTypes.func,
isSkippingWorkout: PropTypes.bool
};
export default ProgramScheduler;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment