Skip to content

Commit 56dc056

Browse files
Merge pull request #194 from topcoder-platform/PROD-2534_force-login-on-course-start
PROD-2534 - Force Users to Log In Upon Start of Lesson
2 parents f360df6 + 4e51e01 commit 56dc056

File tree

12 files changed

+210
-63
lines changed

12 files changed

+210
-63
lines changed

src-ts/tools/learn/course-details/CourseDetailsPage.tsx

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC, useContext, useMemo } from 'react'
1+
import { FC, ReactNode, useContext, useMemo } from 'react'
22
import { Params, useParams } from 'react-router-dom'
33

44
import {
@@ -14,8 +14,11 @@ import {
1414
CoursesProviderData,
1515
CourseTitle,
1616
MyCertificationProgressProviderData,
17+
MyCertificationProgressStatus,
18+
ResourceProviderData,
1719
useCoursesProvider,
18-
useMyCertificationProgress
20+
useMyCertificationProgress,
21+
useResourceProvider
1922
} from '../learn-lib'
2023

2124
import { CourseCurriculum } from './course-curriculum'
@@ -25,20 +28,78 @@ import { PromoCourse } from './promo-course'
2528
const CourseDetailsPage: FC<{}> = () => {
2629

2730
const routeParams: Params<string> = useParams()
28-
const { profile }: ProfileContextData = useContext(profileContext)
31+
const { profile, initialized: profileReady }: ProfileContextData = useContext(profileContext)
32+
33+
const {
34+
provider: resourceProvider,
35+
}: ResourceProviderData = useResourceProvider(routeParams.provider)
2936

3037
const {
3138
course,
32-
ready,
39+
ready: courseReady,
3340
}: CoursesProviderData = useCoursesProvider(routeParams.provider ?? '', routeParams.certification)
3441

35-
const { certificateProgress: progress }: MyCertificationProgressProviderData = useMyCertificationProgress(profile?.userId, routeParams.provider, routeParams.certification)
42+
const {
43+
certificateProgress: progress,
44+
ready: progressReady,
45+
}: MyCertificationProgressProviderData = useMyCertificationProgress(
46+
profile?.userId,
47+
routeParams.provider,
48+
routeParams.certification,
49+
)
3650

51+
const ready: boolean = profileReady && courseReady && (!profile || progressReady)
3752
const breadcrumb: Array<BreadcrumbItemModel> = useMemo(() => [
3853
{ url: '/learn', name: 'Topcoder Academy' },
3954
{ url: `/learn/${routeParams.provider}/${routeParams.certification}`, name: course?.title ?? '' },
4055
], [routeParams, course])
4156

57+
function getDescription(): ReactNode {
58+
if (!course) {
59+
return
60+
}
61+
62+
return progress?.status === MyCertificationProgressStatus.completed ? (
63+
<>
64+
<h3 className='details'>Suggested next steps</h3>
65+
66+
<div className={styles['text']}>
67+
<p>
68+
Now that you have completed the {course.title},
69+
we'd recommend you enroll in another course to continue your learning.
70+
You can view our other courses from the Topcoder Academy course page.
71+
</p>
72+
</div>
73+
</>
74+
) : (
75+
course.keyPoints && (
76+
<>
77+
<h3 className='details'>Why should you complete this course?</h3>
78+
79+
<div
80+
className={styles['text']}
81+
dangerouslySetInnerHTML={{ __html: (course.keyPoints ?? []).join('<br /><br />') }}
82+
></div>
83+
</>
84+
)
85+
)
86+
}
87+
88+
function getFooter(): ReactNode {
89+
if (!resourceProvider) {
90+
return
91+
}
92+
93+
return (
94+
<div className={styles['credits-link']}>
95+
<a href={`//${resourceProvider.url}`} target='_blank' referrerPolicy='no-referrer' rel='noreferrer'>
96+
This course was created by the {resourceProvider.url} community.
97+
<IconOutline.ExternalLinkIcon />
98+
</a>
99+
</div>
100+
)
101+
}
102+
42103
return (
43104
<ContentLayout>
44105
{!ready && (
@@ -51,7 +112,7 @@ const CourseDetailsPage: FC<{}> = () => {
51112
<>
52113
<div className={styles['wrap']}>
53114
<div className={styles['intro-copy']}>
54-
<CourseTitle size='lg' title={course.title} credits={course?.provider} type='webdev' />
115+
<CourseTitle size='lg' title={course.title} credits={course.provider} type='webdev' />
55116

56117
<div
57118
className={styles['text']}
@@ -60,30 +121,7 @@ const CourseDetailsPage: FC<{}> = () => {
60121
</div>
61122

62123
<div className={styles['description']}>
63-
{progress?.status === 'completed' ? (
64-
<>
65-
<h3 className='details'>Suggested next steps</h3>
66-
67-
<div className={styles['text']}>
68-
<p>
69-
Now that you have completed the {course.title},
70-
we'd recommend you enroll in another course to continue your learning.
71-
You can view our other courses from the Topcoder Academy course page.
72-
</p>
73-
</div>
74-
</>
75-
) : (
76-
course.keyPoints && (
77-
<>
78-
<h3 className='details'>Why should you complete this course?</h3>
79-
80-
<div
81-
className={styles['text']}
82-
dangerouslySetInnerHTML={{ __html: (course.keyPoints ?? []).join('<br /><br />') }}
83-
></div>
84-
</>
85-
)
86-
)}
124+
{getDescription()}
87125
<div className={styles['coming-soon']}>
88126
<PromoCourse />
89127
</div>
@@ -93,18 +131,12 @@ const CourseDetailsPage: FC<{}> = () => {
93131
<CourseCurriculum
94132
course={course}
95133
progress={progress}
96-
profileUserId={profile?.userId}
134+
progressReady={progressReady}
135+
profile={profile}
97136
/>
98137
</div>
99138
</div>
100-
{course?.provider === 'freeCodeCamp' && (
101-
<div className={styles['credits-link']}>
102-
<a href='https://freecodecamp.org/' target='_blank' referrerPolicy='no-referrer' rel='noreferrer'>
103-
This course was created by the freeCodeCamp.org community.
104-
<IconOutline.ExternalLinkIcon />
105-
</a>
106-
</div>
107-
)}
139+
{getFooter()}
108140
</>
109141
)}
110142
</ContentLayout>

src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import classNames from 'classnames'
2-
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react'
3-
import { NavigateFunction, useNavigate } from 'react-router-dom'
2+
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react'
3+
import { NavigateFunction, useNavigate, useSearchParams } from 'react-router-dom'
44

5-
import { Button } from '../../../../lib'
5+
import { Button, UserProfile } from '../../../../lib'
66
import {
77
CourseOutline,
88
LearnCourse,
@@ -15,20 +15,24 @@ import {
1515
UpdateMyCertificateProgressActions,
1616
updateMyCertificationsProgressAsync
1717
} from '../../learn-lib'
18-
import { getFccLessonPath, LEARN_PATHS } from '../../learn.routes'
18+
import { authenticateAndStartCourseRoute, getFccLessonPath, LEARN_PATHS } from '../../learn.routes'
1919

2020
import styles from './CourseCurriculum.module.scss'
2121
import { CurriculumSummary } from './curriculum-summary'
2222
import { TcAcademyPolicyModal } from './tc-academy-policy-modal'
2323

2424
interface CourseCurriculumProps {
2525
course: LearnCourse
26-
profileUserId?: number
26+
profile?: UserProfile
2727
progress?: LearnMyCertificationProgress
28+
progressReady?: boolean
2829
}
2930

3031
const CourseCurriculum: FC<CourseCurriculumProps> = (props: CourseCurriculumProps) => {
3132
const navigate: NavigateFunction = useNavigate()
33+
const [searchParams]: any = useSearchParams()
34+
35+
const isLoggedIn: boolean = !!props.profile
3236

3337
const [isTcAcademyPolicyModal, setIsTcAcademyPolicyModal]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false)
3438

@@ -57,32 +61,44 @@ const CourseCurriculum: FC<CourseCurriculumProps> = (props: CourseCurriculumProp
5761
}, [props.course, props.progress, navigate])
5862

5963
/**
60-
* Check if user accepted policy when user clicks start course
61-
* If not, show the policy modal
62-
* otherwise resume(or start) the course
64+
* Handle user click on start course/resume/login button
6365
*/
6466
const handleStartCourseClick: () => void = useCallback(() => {
65-
if (props.progress?.academicHonestyPolicyAcceptedAt) {
66-
handleStartCourse()
67-
return
68-
}
67+
// if user is not logged in, redirect to login page
68+
if (!isLoggedIn) {
69+
// add a flag to the return url to show the academic policy modal
70+
// or resume the course when they're back
71+
window.location.href = authenticateAndStartCourseRoute
72+
return
73+
}
6974

70-
setIsTcAcademyPolicyModal(true)
71-
}, [handleStartCourse, props.progress?.academicHonestyPolicyAcceptedAt])
75+
// Check if user accepted policy and resume(or start) the course
76+
if (props.progress?.academicHonestyPolicyAcceptedAt) {
77+
handleStartCourse()
78+
return
79+
}
80+
81+
// show the academic policy modal before starting a new course
82+
setIsTcAcademyPolicyModal(true)
83+
}, [
84+
handleStartCourse,
85+
isLoggedIn,
86+
props.progress?.academicHonestyPolicyAcceptedAt,
87+
])
7288

7389
/**
7490
* When user clicks accept inside the policy modal,
7591
* if there's no progress on the course yet, create the progress (this will also mark policy as accepted)
7692
* otherwise send a PUT request to expressly accept the policy
7793
*/
7894
const handlePolicyAccept: () => void = useCallback(async () => {
79-
if (!props.profileUserId) {
95+
if (!props.profile) {
8096
return
8197
}
8298

8399
if (!props.progress?.id) {
84100
await startMyCertificationsProgressAsync(
85-
props.profileUserId,
101+
props.profile.userId,
86102
props.course.certificationId,
87103
props.course.id,
88104
{
@@ -104,10 +120,20 @@ const CourseCurriculum: FC<CourseCurriculumProps> = (props: CourseCurriculumProp
104120
props.course.certificationId,
105121
props.course.id,
106122
props.course.modules,
107-
props.profileUserId,
108-
props.progress,
123+
props.profile,
124+
props.progress?.id,
109125
])
110126

127+
/**
128+
* If the url has a "start-course" search param,
129+
* proceed as if the user just clicked "Start course" button
130+
*/
131+
useEffect(() => {
132+
if (props.progressReady && isLoggedIn && searchParams.get('start-course') !== null) {
133+
handleStartCourseClick()
134+
}
135+
}, [handleStartCourseClick, isLoggedIn, props.progressReady, searchParams])
136+
111137
return (
112138
<>
113139
<div className={styles['wrap']}>
@@ -128,6 +154,7 @@ const CourseCurriculum: FC<CourseCurriculumProps> = (props: CourseCurriculumProp
128154
completedPercentage={completedPercentage}
129155
completed={isCompleted}
130156
completedDate={props.progress?.completedDate}
157+
isLoggedIn={isLoggedIn}
131158
/>
132159

133160
<div className={styles['course-outline']}>

src-ts/tools/learn/course-details/course-curriculum/curriculum-summary/CurriculumSummary.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface CurriculumSummaryProps {
1111
completedPercentage?: number
1212
course: LearnCourse
1313
inProgress?: boolean
14+
isLoggedIn: boolean
1415
onClickCertificateBtn?: () => void
1516
onClickMainBtn: () => void
1617
}
@@ -20,6 +21,12 @@ const CurriculumSummary: FC<CurriculumSummaryProps> = (props: CurriculumSummaryP
2021
const inProgress: boolean|undefined = props.inProgress
2122
const completed: boolean|undefined = props.completed
2223

24+
const mainBtnLabel: string = !props.isLoggedIn ? 'Log in' : (
25+
completed ? 'Review' : (
26+
inProgress ? 'Resume' : 'Start Course'
27+
)
28+
)
29+
2330
const title: ReactNode = useMemo(() => {
2431
if (!completed || !props.completedDate) {
2532
return 'In Progress'
@@ -62,7 +69,7 @@ const CurriculumSummary: FC<CurriculumSummaryProps> = (props: CurriculumSummaryP
6269
<Button
6370
buttonStyle={completed ? 'secondary' : 'primary'}
6471
size='md'
65-
label={completed ? 'Review' : (inProgress ? 'Resume' : 'Start Course')}
72+
label={mainBtnLabel}
6673
onClick={props.onClickMainBtn}
6774
/>
6875
</div>

src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ import styles from './FreeCodeCamp.module.scss'
3131
import { TitleNav } from './title-nav'
3232

3333
const FreeCodeCamp: FC<{}> = () => {
34-
const { profile }: ProfileContextData = useContext(profileContext)
34+
const {
35+
profile,
36+
initialized: profileReady,
37+
}: ProfileContextData = useContext(profileContext)
38+
const isLoggedIn: boolean = !!profile
3539

3640
const navigate: NavigateFunction = useNavigate()
3741
const routeParams: Params<string> = useParams()
@@ -59,7 +63,7 @@ const FreeCodeCamp: FC<{}> = () => {
5963
lessonParam,
6064
)
6165

62-
const ready: boolean = courseDataReady && lessonReady && progressReady
66+
const ready: boolean = profileReady && courseDataReady && lessonReady && (!isLoggedIn || progressReady)
6367

6468
const breadcrumb: Array<BreadcrumbItemModel> = useMemo(() => [
6569
{ url: '/learn', name: 'Topcoder Academy' },
@@ -200,14 +204,21 @@ const FreeCodeCamp: FC<{}> = () => {
200204
* if not, redirect user to course details page to accept the policy
201205
*/
202206
useLayoutEffect(() => {
203-
if (ready && !certificateProgress?.academicHonestyPolicyAcceptedAt) {
207+
if (ready && !(isLoggedIn && certificateProgress?.academicHonestyPolicyAcceptedAt)) {
204208
const coursePath: string = getCoursePath(
205209
providerParam,
206210
certificationParam
207211
)
208212
navigate(coursePath)
209213
}
210-
}, [ready, certificateProgress, providerParam, certificationParam, navigate])
214+
}, [
215+
ready,
216+
certificateProgress,
217+
providerParam,
218+
certificationParam,
219+
navigate,
220+
isLoggedIn,
221+
])
211222

212223
return (
213224
<>

src-ts/tools/learn/learn-lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './certifications-provider'
2+
export * from './resource-provider-provider'
23
export * from './collapsible-pane'
34
export * from './courses-provider'
45
export * from './curriculum-summary'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './resource-provider-functions'
2+
export * from './resource-provider-data.model'
3+
export * from './resource-provider.provider'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ResourceProvider } from './resource-provider-functions'
2+
3+
export interface ResourceProviderData {
4+
loading: boolean
5+
provider?: ResourceProvider
6+
ready: boolean
7+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './resource-provider.model'
2+
export * from './resource-provider.store'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface ResourceProvider {
2+
attributionStatement: string
3+
id: string
4+
name: string
5+
url: string
6+
}

0 commit comments

Comments
 (0)