Created
May 22, 2023 10:28
-
-
Save jkhaui/c34c4fd703a1e8be3c3b3f2f8f3e7854 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); | |
useEffect(() => { | |
if (!isProgramsOptionalWorkouts || !weeks || !workouts) { | |
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]; | |
setLastWeekWithIncompleteProgressIndex( | |
weeks.findLastIndex( | |
week => | |
!week.locked && | |
week.completed + (isProgramsOptionalWorkouts ? week?.skipped : 0) !== | |
week.total && | |
(week.completed > 0 || week.skipped > 0) | |
) + (doesUpNextWorkoutExistInCurrentWeek ? 0 : 1) | |
); | |
}, [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 (shouldRefetchRef.current === false || !isProgramsOptionalWorkouts) { | |
return; | |
} | |
refetch().then(({ data: res }) => { | |
const data = { | |
result: { | |
...res | |
} | |
}; | |
const inProgressWeekWorkouts = transformProgramScheduler(data, { | |
level: 1 | |
})?.workouts; | |
// setUpNextWorkout(inProgressWeekWorkouts[0]); | |
}); | |
}, [lastWeekWithIncompleteProgressIndex]); | |
useEffect(() => { | |
if ( | |
lastWeekWithIncompleteProgressIndex === -1 || | |
!isProgramsOptionalWorkouts | |
) { | |
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) | |
); | |
const maybeUpNextWorkout = | |
inProgressWeekWorkouts[lastCompletedOrSkippedWorkoutIndex + 1]; | |
if (maybeUpNextWorkout) { | |
// setUpNextWorkout(maybeUpNextWorkout); | |
return; | |
} | |
shouldRefetchRef.current = true; | |
setLastWeekWithIncompleteProgressIndex(x => x + 1); | |
}); | |
return; | |
} | |
const upNextWorkoutIndex = | |
workouts.findLastIndex( | |
workout => | |
workout.completed === true || | |
(isProgramsOptionalWorkouts ? workout?.skipped : true) | |
) + 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