Created
May 24, 2023 00:21
-
-
Save jkhaui/e000e995198b657978423ccdcc906ecb to your computer and use it in GitHub Desktop.
This file contains hidden or 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); | |
const [shouldRefetch, setShouldRefetch] = useState(false); | |
useEffect(() => { | |
console.log(141, 'should', shouldRefetch); | |
if ( | |
shouldRefetch || | |
!isProgramsOptionalWorkouts || | |
weeks.length === 0 || | |
workouts.length === 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) | |
); | |
console.log(_lastWeekWithIncompleteProgressIndex, 170); | |
if (_lastWeekWithIncompleteProgressIndex === -1) { | |
console.log( | |
weeks.findLastIndex( | |
week => | |
week.completed || (isProgramsOptionalWorkouts ? week.skipped : true) | |
), | |
'findLastIndex' | |
); | |
_lastWeekWithIncompleteProgressIndex = | |
weeks.findLastIndex( | |
week => | |
week.completed || (isProgramsOptionalWorkouts ? week.skipped : true) | |
) + 1; | |
} | |
console.log(_lastWeekWithIncompleteProgressIndex, '_lastWeekWith'); | |
console.log( | |
doesUpNextWorkoutExistInCurrentWeek, | |
_lastWeekWithIncompleteProgressIndex === selectedWeek, | |
shouldRefetch | |
); | |
if (_lastWeekWithIncompleteProgressIndex === selectedWeek) { | |
if (doesUpNextWorkoutExistInCurrentWeek) { | |
setLastWeekWithIncompleteProgressIndex( | |
_lastWeekWithIncompleteProgressIndex | |
); | |
} | |
return; | |
} | |
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 | |
); | |
console.log(workouts); | |
console.log(haveAllWorkoutsInWeekBeenCompletedOrSkipped, 'skipped'); | |
const lastWorkoutOfWeekHasBeenCompletedOrSkipped = | |
workouts.findLastIndex( | |
workout => workout.completed || workout.skipped | |
) + | |
1 === | |
workouts.length; | |
console.log('a'); | |
if (doFutureWeeksWithCompletedOrSkippedWorkoutsExist) { | |
// const isEdgeCaseWithUpNextInSecondConsecutiveWeek = | |
setLastWeekWithIncompleteProgressIndex( | |
_lastWeekWithIncompleteProgressIndex | |
); | |
console.log('b'); | |
return; | |
} | |
if (!doFutureWeeksWithCompletedOrSkippedWorkoutsExist) { | |
console.log('c', haveAllWorkoutsInWeekBeenCompletedOrSkipped); | |
if ( | |
(haveAllWorkoutsInWeekBeenCompletedOrSkipped || | |
lastWorkoutOfWeekHasBeenCompletedOrSkipped) && | |
_lastWeekWithIncompleteProgressIndex === selectedWeek | |
) { | |
console.log( | |
'd', | |
haveAllWorkoutsInWeekBeenCompletedOrSkipped, | |
lastWorkoutOfWeekHasBeenCompletedOrSkipped, | |
_lastWeekWithIncompleteProgressIndex, | |
selectedWeek | |
); | |
setLastWeekWithIncompleteProgressIndex(x => x + 1); | |
return; | |
} | |
} | |
} | |
console.log('e'); | |
// setUpNextWorkout(workouts[0]); | |
}, [ | |
JSON.stringify(weeks), | |
JSON.stringify(workouts), | |
selectedWeek, | |
shouldRefetch | |
]); | |
console.log( | |
lastWeekWithIncompleteProgressIndex, | |
'lastWeekWithIncompleteProg' | |
); | |
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 hasRefetchedRef = useRef(false); | |
useEffect(() => { | |
if ( | |
lastWeekWithIncompleteProgressIndex === -1 || | |
!isProgramsOptionalWorkouts || | |
weeks.length === 0 || | |
workouts.length === 0 | |
) { | |
return; | |
} | |
console.log(shouldRefetch, 'shouldRefetch'); | |
if (shouldRefetch && lastWeekWithIncompleteProgressIndex !== selectedWeek) { | |
// setShouldRefetch(false); | |
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; | |
} | |
}); | |
setShouldRefetch(false); | |
return; | |
} | |
let upNextWorkoutIndex = workouts.findLastIndex( | |
workout => | |
workout.completed === true || | |
(isProgramsOptionalWorkouts ? workout?.skipped : true) | |
); | |
if (upNextWorkoutIndex !== -1) { | |
upNextWorkoutIndex = upNextWorkoutIndex + 1; | |
const upNextWorkoutExists = workouts[upNextWorkoutIndex]; | |
if (upNextWorkoutExists) { | |
setUpNextWorkout(workouts[upNextWorkoutIndex]); | |
} else { | |
// If we get `undefined`, then it's most likely because the workout completed/skipped is the last workout in | |
// the week. Therefore, we need to trigger a refetch to search the next week for the up next workout. | |
// shouldRefetchRef.current = true; | |
// setShouldRefetch(true); | |
} | |
} | |
}, [ | |
JSON.stringify(weeks), | |
JSON.stringify(workouts), | |
lastWeekWithIncompleteProgressIndex, | |
selectedWeek, | |
shouldRefetch | |
]); | |
useEffect(() => { | |
if ( | |
// These initial fetch checks are a hack to prevent the incorrect triggering of the refetch callback on initial load. | |
initialFetchRef.current && | |
// initialFetchComplete && | |
lastWeekWithIncompleteProgressIndex !== -1 && | |
lastWeekWithIncompleteProgressIndex !== selectedWeek | |
) { | |
console.log( | |
'REFETCH TRIGGERED', | |
lastWeekWithIncompleteProgressIndex, | |
selectedWeek | |
); | |
setShouldRefetch(true); | |
// shouldRefetchRef.current = true; | |
} | |
}, [lastWeekWithIncompleteProgressIndex, selectedWeek]); | |
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