Created
May 23, 2023 02:41
-
-
Save jkhaui/926ed3ff6fa6468f689dd9d6ccfb42aa to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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