diff --git a/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/CurriculumCard.module.scss b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/CurriculumCard.module.scss index 713d8f818..90a0e6060 100644 --- a/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/CurriculumCard.module.scss +++ b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/CurriculumCard.module.scss @@ -127,7 +127,7 @@ } .bottomActions { - @media (min-width: 1151px) { + @media (min-width: 1150px) { display: none; } } diff --git a/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/course-card/CourseCard.tsx b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/course-card/CourseCard.tsx index ba0757d8d..1df6660a2 100644 --- a/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/course-card/CourseCard.tsx +++ b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/course-card/CourseCard.tsx @@ -13,7 +13,11 @@ import { useHoursEstimateToRange, UserCertificationProgressStatus, } from '../../../../learn-lib' -import { getCertificatePath, getCoursePath } from '../../../../learn.routes' +import { + getCertificatePath, + getCoursePath, + getLessonPathFromCurrentLesson, +} from '../../../../learn.routes' import CurriculumCard from '../CurriculumCard' import styles from './CourseCard.module.scss' @@ -58,9 +62,10 @@ const CourseCard: FC = (props: CourseCardProps) => { buttonStyle='primary' size='xs' label='Resume' - route={getCoursePath( + route={getLessonPathFromCurrentLesson( props.provider, props.certification.certification, + props.progress?.currentLesson, )} /> ) diff --git a/src-ts/tools/learn/certification-details/page-layout/PageLayout.module.scss b/src-ts/tools/learn/certification-details/page-layout/PageLayout.module.scss index 1c16197c2..1fbc5de8e 100644 --- a/src-ts/tools/learn/certification-details/page-layout/PageLayout.module.scss +++ b/src-ts/tools/learn/certification-details/page-layout/PageLayout.module.scss @@ -6,7 +6,7 @@ gap: $space-xxxxl; position: relative; - @media (min-width: 1151px) { + @media (min-width: 1150px) { padding-right: calc(445px + $space-xxl); } @@ -30,7 +30,7 @@ padding-right: calc(40vw + $space-xxl * 2); } - @media (min-width: 1151px) { + @media (min-width: 1150px) { padding-right: calc(445px + $space-xxxl * 2); } } diff --git a/src-ts/tools/learn/course-details/CourseDetailsPage.tsx b/src-ts/tools/learn/course-details/CourseDetailsPage.tsx index 068f71ae5..42743c501 100644 --- a/src-ts/tools/learn/course-details/CourseDetailsPage.tsx +++ b/src-ts/tools/learn/course-details/CourseDetailsPage.tsx @@ -17,9 +17,6 @@ import { CoursesProviderData, CourseTitle, ResourceProviderData, - TCACertification, - TCACertificationsProviderData, - useGetAllTCACertifications, useGetCertification, useGetCourses, useGetResourceProvider, @@ -31,8 +28,8 @@ import { import { getCoursePath } from '../learn.routes' import { CourseCurriculum } from './course-curriculum' -import styles from './CourseDetailsPage.module.scss' import { TCACertificationBanner } from './tca-certification-banner' +import styles from './CourseDetailsPage.module.scss' const CourseDetailsPage: FC<{}> = () => { @@ -198,6 +195,7 @@ const CourseDetailsPage: FC<{}> = () => { /> diff --git a/src-ts/tools/learn/course-details/tca-certification-banner/TCACertificationBanner.module.scss b/src-ts/tools/learn/course-details/tca-certification-banner/TCACertificationBanner.module.scss index 1ab06a8ea..bfd6670e9 100644 --- a/src-ts/tools/learn/course-details/tca-certification-banner/TCACertificationBanner.module.scss +++ b/src-ts/tools/learn/course-details/tca-certification-banner/TCACertificationBanner.module.scss @@ -5,6 +5,10 @@ border-radius: $space-sm; padding: $space-xxl; + + @include ltemd { + padding: $space-lg; + } } .header { @@ -14,6 +18,7 @@ svg { @include icon-size(47); + flex: 0 0 auto; } &Content { @@ -27,6 +32,12 @@ } } +.certTitle { + display: flex; + align-items: flex-start; + gap: $space-xs; +} + .desc { margin-top: $space-lg; } @@ -40,4 +51,46 @@ font-weight: $font-weight-bold; color: $turq-160; text-transform: uppercase; +} + +.externalLink { + display: flex; + height: 24px; + align-items: center; + color: $turq-160; + + svg { + @include icon-lg; + } +} + +.statusBox { + display: flex; + align-items: center; + + background: $black-5; + border-radius: $space-sm; + + margin-top: $space-lg; + padding: $space-sm; + padding-left: 0; + + .icon { + display: flex; + width: 40px; + height: 24px; + align-items: center; + justify-content: center; + flex: 0 0 auto; + + color: $black-40; + + svg { + @include icon-xxl; + } + + &:global(.green) { + color: $turq-75; + } + } } \ No newline at end of file diff --git a/src-ts/tools/learn/course-details/tca-certification-banner/TCACertificationBanner.tsx b/src-ts/tools/learn/course-details/tca-certification-banner/TCACertificationBanner.tsx index 548b97961..a9a945c06 100644 --- a/src-ts/tools/learn/course-details/tca-certification-banner/TCACertificationBanner.tsx +++ b/src-ts/tools/learn/course-details/tca-certification-banner/TCACertificationBanner.tsx @@ -1,19 +1,44 @@ /* eslint-disable react/no-danger */ -import { FC, useMemo } from 'react' +import { FC, ReactNode, useMemo } from 'react' import { Link } from 'react-router-dom' import classNames from 'classnames' +import { IconOutline, IconSolid } from '../../../../lib' import { CertificateBadgeIcon, + LearnUserCertificationProgress, TCACertification, + TCACertificationProgressProviderData, TCACertificationsProviderData, useGetAllTCACertifications, + useGetTCACertificationProgress, + useGetUserCertifications, + UserCertificationProgressStatus, + UserCertificationsProviderData, } from '../../learn-lib' import { getTCACertificationPath } from '../../learn.routes' import styles from './TCACertificationBanner.module.scss' +interface ProgressByIdCollection { + [key: string]: LearnUserCertificationProgress +} + +function getStatusBox(icon: ReactNode, text: string, theme: string = 'gray'): ReactNode { + return ( +
+
+ {icon} +
+
+ {text} +
+
+ ) +} + export interface TCACertificationBannerProps { + userId?: number className?: string fccCertificateId?: string } @@ -30,12 +55,71 @@ const TCACertificationBanner: FC = (props: TCACerti )) ), [tcaCertifications, props.fccCertificateId]) + // Fetch Enrollment status & progress + const { + progress: certifProgress, + ready: certifProgressReady, + }: TCACertificationProgressProviderData = useGetTCACertificationProgress( + props.userId as unknown as string, + certification?.dashedName as string, + { enabled: !!certification && !!props.userId }, + ) + + // Fetch the User's progress for all the tca certification's courses + // so we can show their progress even before they enroll with the certification + const { + progresses: certsProgress, + }: UserCertificationsProviderData = useGetUserCertifications('freeCodeCamp', { + enabled: certifProgressReady && !certifProgress, + }) + + const progressById: ProgressByIdCollection = useMemo(() => ( + certsProgress?.reduce((all, progress) => { + all[progress.certificationId] = progress + return all + }, {} as ProgressByIdCollection) ?? {} + ), [certsProgress]) + if (!certification) { return <> } const certifUrl: string = getTCACertificationPath(certification.dashedName) + function renderStatusBox(): ReactNode { + if (!certification) { + return <> + } + + const coursesCount: number = certification.coursesCount + const completedCoursesCount: number = certifProgress + ? Math.round(coursesCount * (certifProgress.certificationProgress / 100)) + : certification.certificationResources.filter(d => ( + progressById[d.freeCodeCampCertification.fccId]?.status === UserCertificationProgressStatus.completed + )).length + + if (!completedCoursesCount) { + return certifProgress ? getStatusBox( + , + 'Begin working towards earning this Topcoder Certification by starting this course today!', + ) : <> + } + + if (completedCoursesCount === 1) { + return getStatusBox( + , + `Good job! You are making progress with 1 of ${coursesCount} required courses.`, + 'green', + ) + } + + return getStatusBox( + , + `You have completed ${completedCoursesCount} of ${coursesCount} required courses.`, + 'green', + ) + } + return (
@@ -47,23 +131,34 @@ const TCACertificationBanner: FC = (props: TCACerti
This course is part of a topcoder certification:
-
+
{certification.title} + {!!certifProgress && ( + + + + )}
-

- {certification.description} -

- - - Learn more - + {!certifProgress && ( +

+ {certification.description} +

+ )} + + {renderStatusBox()} + + {!certifProgress && ( + + Learn more + + )}
) } diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications.provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications.provider.tsx index d4bfdcf1f..d3408595b 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications.provider.tsx +++ b/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications.provider.tsx @@ -9,8 +9,13 @@ import { UserCertificationInProgress } from './user-certification-in-progress.mo import { LearnUserCertificationProgress, UserCertificationProgressStatus } from './user-certifications-functions' import { UserCertificationsProviderData } from './user-certifications-provider-data.model' +interface GetUserCertificationsOptions { + enabled?: boolean +} + export function useGetUserCertifications( provider: string = 'freeCodeCamp', + options: GetUserCertificationsOptions = {} as GetUserCertificationsOptions, ): UserCertificationsProviderData { const profileContextData: ProfileContextData = useContext(profileContext) const userId: number | undefined = profileContextData?.profile?.userId @@ -25,7 +30,7 @@ export function useGetUserCertifications( const url: string = learnUrlGet('certification-progresses', params) const { data, error }: SWRResponse> = useSWR(url, { - isPaused: () => !userId, + isPaused: () => !userId || options?.enabled === false, }) const loading: boolean = !data && !error