From b2086d4b41743d903f5136b0461461fc3fd08f0c Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 23 Jan 2023 11:17:32 +0200 Subject: [PATCH 1/5] TCA-870 init tca certs --- .../learn/learn-lib/data-providers/index.ts | 1 + .../tca-certifications-provider-data.model.ts | 7 + .../tca-certifications.provider.tsx | 96 +++++--- .../index.ts | 3 + .../user-completed-tca-certification.model.ts | 5 + ...-tca-certifications-provider-data.model.ts | 7 + ...-completed-tca-certifications.provider.tsx | 48 ++++ src-ts/tools/learn/learn.routes.tsx | 20 ++ .../CertificateView.module.scss | 80 +++++++ .../certificate-view/CertificateView.tsx | 207 ++++++++++++++++++ .../action-button/ActionButton.module.scss | 21 ++ .../action-button/ActionButton.tsx | 36 +++ .../certificate-view/action-button/index.ts | 1 + .../certificate/Certificate.module.scss | 168 ++++++++++++++ .../certificate/Certificate.tsx | 86 ++++++++ .../CertificateBgPattern.module.scss | 58 +++++ .../CertificateBgPattern.tsx | 19 ++ .../certificate-bg-pattern/index.ts | 1 + .../certificate-bg-pattern/pattern-bg.png | Bin 0 -> 48910 bytes .../certificate-bg-pattern/wave-bg.png | Bin 0 -> 899 bytes .../course-card/CourseCard.module.scss | 52 +++++ .../certificate/course-card/CourseCard.tsx | 41 ++++ .../certificate/course-card/index.ts | 1 + .../certificate/course-card/wave-bg.png | Bin 0 -> 2399 bytes .../certificate/includes.scss | 17 ++ .../certificate-view/certificate/index.ts | 1 + .../tca-certificate/certificate-view/index.ts | 2 + .../use-certificate-scaling.hook.tsx | 27 +++ src-ts/tools/learn/tca-certificate/index.ts | 2 + .../my-certificate/MyTCACertificate.tsx | 45 ++++ .../tca-certificate/my-certificate/index.ts | 1 + .../UserCertificate.module.scss | 5 + .../user-certificate/UserCertificate.tsx | 73 ++++++ .../tca-certificate/user-certificate/index.ts | 1 + 34 files changed, 1103 insertions(+), 29 deletions(-) create mode 100644 src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/index.ts create mode 100644 src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certification.model.ts create mode 100644 src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certifications-provider-data.model.ts create mode 100644 src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certifications.provider.tsx create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.module.scss create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.module.scss create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.tsx create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/action-button/index.ts create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/Certificate.module.scss create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/Certificate.tsx create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/CertificateBgPattern.module.scss create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/CertificateBgPattern.tsx create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/index.ts create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/pattern-bg.png create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/wave-bg.png create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/CourseCard.module.scss create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/CourseCard.tsx create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/index.ts create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/wave-bg.png create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/includes.scss create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/certificate/index.ts create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/index.ts create mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/use-certificate-scaling.hook.tsx create mode 100644 src-ts/tools/learn/tca-certificate/index.ts create mode 100644 src-ts/tools/learn/tca-certificate/my-certificate/MyTCACertificate.tsx create mode 100644 src-ts/tools/learn/tca-certificate/my-certificate/index.ts create mode 100644 src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.module.scss create mode 100644 src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.tsx create mode 100644 src-ts/tools/learn/tca-certificate/user-certificate/index.ts diff --git a/src-ts/tools/learn/learn-lib/data-providers/index.ts b/src-ts/tools/learn/learn-lib/data-providers/index.ts index 0d0093e10..1e892d208 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/index.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/index.ts @@ -4,4 +4,5 @@ export * from './lesson-provider' export * from './resource-provider-provider' export * from './user-certifications-provider' export * from './user-completed-certifications-provider' +export * from './user-completed-tca-certifications-provider' export * from './tca-certifications-provider' diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certifications-provider-data.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certifications-provider-data.model.ts index 451f60f0d..57d4a914f 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certifications-provider-data.model.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certifications-provider-data.model.ts @@ -6,3 +6,10 @@ export interface TCACertificationsProviderData { loading: boolean ready: boolean } + +export interface TCACertificationProviderData { + certification: TCACertification + error: boolean + loading: boolean + ready: boolean +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certifications.provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certifications.provider.tsx index 8508d7165..d5894f186 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certifications.provider.tsx +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certifications.provider.tsx @@ -3,22 +3,46 @@ /* eslint-disable default-param-last */ import useSWR, { SWRConfiguration, SWRResponse } from 'swr' +import { LEARN_PATHS } from '../../../learn.routes' import { learnUrlGet } from '../../functions' import { useSwrCache } from '../../learn-swr' -import { TCACertificationsProviderData } from './tca-certifications-provider-data.model' +import { TCACertificationProviderData, TCACertificationsProviderData } from './tca-certifications-provider-data.model' import { TCACertification } from './tca-certification.model' interface TCACertificationsAllProviderOptions { enabled?: boolean } +const TCACertificationMock: TCACertification[] = [{ + id: 1, + title: 'Web Development Fundamentals', + description: 'The Web Developer Fundamentals certification will teach you the basics of HTML, CSS, javascript, front end libraries and will also introduce you to backend development.', + estimatedCompletionTime: 4, + learnerLevel: 'Beginner', + sequentialCourses: false, + status: 'active', + certificationCategoryId: '', + skills: ['HTML', 'CSS', 'JavaScript', 'HTML', 'CSS', 'JavaScript', 'HTML', 'CSS', 'JavaScript', 'HTML', 'CSS', 'JavaScript', 'HTML', 'CSS', 'JavaScript'], +}, +{ + id: 2, + title: 'Data Science Fundamentals', + description: 'The Data Science Fundamentals certification will teach you the basics of scientific computing, Data Analysis and machine learning while using Python. Additionally, you will learn about data visualization.', + estimatedCompletionTime: 14, + status: 'active', + sequentialCourses: false, + learnerLevel: 'Expert', + certificationCategoryId: '', + skills: ['Python', 'TensorFlow', 'JSON'], +}] + export function useGetAllTCACertifications( options?: TCACertificationsAllProviderOptions, ): TCACertificationsProviderData { const url: string = learnUrlGet( - 'topcoder-certifications', + LEARN_PATHS.tcaCertifications, ) const swrCacheConfig: SWRConfiguration = useSwrCache(url) @@ -35,37 +59,51 @@ export function useGetAllTCACertifications( } } -// TODO: remove when integrated with API -export function useGetAllTCACertificationsMOCK(): TCACertificationsProviderData { - const data: TCACertification[] = [{ - id: 1, - title: 'Web Development Fundamentals', - description: 'The Web Developer Fundamentals certification will teach you the basics of HTML, CSS, javascript, front end libraries and will also introduce you to backend development.', - estimatedCompletionTime: 4, - learnerLevel: 'Beginner', - sequentialCourses: false, - status: 'active', - certificationCategoryId: '', - skills: ['HTML', 'CSS', 'JavaScript', 'HTML', 'CSS', 'JavaScript', 'HTML', 'CSS', 'JavaScript', 'HTML', 'CSS', 'JavaScript', 'HTML', 'CSS', 'JavaScript'], - }, - { - id: 2, - title: 'Data Science Fundamentals', - description: 'The Data Science Fundamentals certification will teach you the basics of scientific computing, Data Analysis and machine learning while using Python. Additionally, you will learn about data visualization.', - estimatedCompletionTime: 14, - status: 'active', - sequentialCourses: false, - learnerLevel: 'Expert', - certificationCategoryId: '', - skills: ['Python', 'TensorFlow', 'JSON'], - }] - - const error = {} +export function useGetTCACertification( + certification: string, + options?: TCACertificationsAllProviderOptions, +): TCACertificationProviderData { + + const url: string = learnUrlGet( + LEARN_PATHS.tcaCertifications, + certification, + ) + const swrCacheConfig: SWRConfiguration = useSwrCache(url) + + const { data, error }: SWRResponse = useSWR(url, { + ...swrCacheConfig, + isPaused: () => options?.enabled === false, + }) return { - certifications: data ?? [], + certification: data, error: !!error, loading: !data, ready: !!data, } } + +// TODO: remove when integrated with API +export function useGetTCACertificationMOCK( + certification: string, +): TCACertificationProviderData { + + const data: TCACertification = TCACertificationMock[certification as any] + + return { + certification: data, + error: false, + loading: !data, + ready: !!data, + } +} + +// TODO: remove when integrated with API +export function useGetAllTCACertificationsMOCK(): TCACertificationsProviderData { + return { + certifications: TCACertificationMock ?? [], + error: false, + loading: !TCACertificationMock, + ready: !!TCACertificationMock, + } +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/index.ts b/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/index.ts new file mode 100644 index 000000000..40bf84940 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/index.ts @@ -0,0 +1,3 @@ +export * from './user-completed-tca-certifications-provider-data.model' +export * from './user-completed-tca-certifications.provider' +export * from './user-completed-tca-certification.model' diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certification.model.ts b/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certification.model.ts new file mode 100644 index 000000000..2e420ed42 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certification.model.ts @@ -0,0 +1,5 @@ +export interface UserCompletedTCACertification { + status: string + completedDate: string + trackType: string +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certifications-provider-data.model.ts b/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certifications-provider-data.model.ts new file mode 100644 index 000000000..925715b33 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certifications-provider-data.model.ts @@ -0,0 +1,7 @@ +import { UserCompletedTCACertification } from './user-completed-tca-certification.model' + +export interface UserCompletedTCACertificationsProviderData { + certifications: ReadonlyArray + loading: boolean + ready: boolean +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certifications.provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certifications.provider.tsx new file mode 100644 index 000000000..80e6905d5 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certifications.provider.tsx @@ -0,0 +1,48 @@ +import useSWR, { SWRResponse } from 'swr' + +import { learnUrlGet } from '../../functions' + +import { UserCompletedTCACertification } from './user-completed-tca-certification.model' +import { UserCompletedTCACertificationsProviderData } from './user-completed-tca-certifications-provider-data.model' + +const COMPLETED_CERTS_MOCK = [ + { status: 'comleted', trackType: 'web dev', completedDate: '' }, +] + +export function useGetUserTCACompletedCertifications( + userId?: number, + certification?: string, +): UserCompletedTCACertificationsProviderData { + + // TODO: update to actual API endpoint URL when ready + const url: string = learnUrlGet('completed-certifications', `${userId}`) + + const { data, error }: SWRResponse> = useSWR(url) + + let certifications: ReadonlyArray = data ?? [] + + if (certification) { + certifications = certifications + .filter(c => (!certification || c.certification === certification)) + } + + return { + certifications, + loading: !data && !error, + ready: !!data || !!error, + } +} + +// TODO: remove when API ready +export function useGetUserTCACompletedCertificationsMOCK( + userId?: number, + certification?: string, +): UserCompletedTCACertificationsProviderData { + const data = COMPLETED_CERTS_MOCK + + return { + certifications: data, + loading: !data, + ready: !!data, + } +} diff --git a/src-ts/tools/learn/learn.routes.tsx b/src-ts/tools/learn/learn.routes.tsx index e7272e050..75f94fe67 100644 --- a/src-ts/tools/learn/learn.routes.tsx +++ b/src-ts/tools/learn/learn.routes.tsx @@ -11,6 +11,7 @@ const UserCertificate: LazyLoadedComponent = lazyLoad(() => import('./course-cer const FreeCodeCamp: LazyLoadedComponent = lazyLoad(() => import('./free-code-camp'), 'FreeCodeCamp') const MyLearning: LazyLoadedComponent = lazyLoad(() => import('./my-learning'), 'MyLearning') const LandingLearn: LazyLoadedComponent = lazyLoad(() => import('./Learn')) +const MyTCACertificate: LazyLoadedComponent = lazyLoad(() => import('./tca-certificate'), 'MyTCACertificate') export enum LEARN_PATHS { certificate = '/certificate', @@ -20,6 +21,7 @@ export enum LEARN_PATHS { fcc = '/learn/fcc', root = '/learn', startCourseRouteFlag = 'start-course', + tcaCertifications = 'tca-certifications', } export const rootRoute: string = LEARN_PATHS.root @@ -70,6 +72,14 @@ export function getUserCertificateSsr( return `${LearnConfig.CERT_DOMAIN}/${handle}/${provider}/${certification}/${encodeURI(title)}` } +export function getUserTCACertificateSsr( + certification: string, + handle: string, + title: string, +): string { + return `${LearnConfig.CERT_DOMAIN}/${handle}/tca/${certification}/${encodeURI(title)}` +} + export function getUserCertificateUrl( provider: string, certification: string, @@ -82,6 +92,10 @@ export function getViewStyleParamKey(): string { return Object.keys(LearnConfig.CERT_ALT_PARAMS)[0] } +export function getTCACertificationPath(certification: string): string { + return `${LEARN_PATHS.root}/${LEARN_PATHS.tcaCertifications}/${certification}` +} + export const learnRoutes: ReadonlyArray = [ { children: [ @@ -127,6 +141,12 @@ export const learnRoutes: ReadonlyArray = [ id: 'My Learning', route: 'my-learning', }, + { + children: [], + element: , + id: 'My TCA Certification', + route: 'tca-certifications/:certification/certificate', + }, ], element: , id: toolTitle, diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.module.scss b/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.module.scss new file mode 100644 index 000000000..d9b0568b9 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.module.scss @@ -0,0 +1,80 @@ +@import '../../../../lib/styles/includes'; + +.wrap { + padding-top: $space-xxxxl; + padding-bottom: calc($space-xxxxl + $space-xs); + flex: 99 1 auto; + display: flex; + + background: $tc-grad15; +} + +.content-wrap { + display: flex; + @include pagePaddings; + margin: auto; + width: 100%; + justify-content: center; + + gap: $space-xxxxl; + + @include ltemd { + flex-direction: column; + margin: 0 auto auto; + } +} + +.btns-wrap { + display: flex; + flex-direction: column; + align-items: center; + + gap: $space-sm; + + &:last-child { + margin-top: auto; + } + + @include ltemd { + flex-direction: row; + &:last-child { + justify-content: center; + } + } +} + +.certificate-wrap { + aspect-ratio: 1.25715; + width: 880px; + + background: #fff; + + box-shadow: 0 20px 36px rgba($tc-black, 0.22); + + &:global(.large-container) { + aspect-ratio: unset; + @include socialPreviewImg; + } +} + +.share-btn:global(.button.icon) { + @include icon-mxx; + border-radius: 50%; + + color: $tc-white; + border: $border solid $tc-white; + + display: flex; + align-items: center; + justify-content: center; + + padding: $space-sm; + + &:hover { + background: transparent; + } + + svg { + @include icon-xxl; + } +} diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx b/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx new file mode 100644 index 000000000..12d2ecb13 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx @@ -0,0 +1,207 @@ +import { FC, MutableRefObject, useCallback, useEffect, useMemo, useRef } from 'react' +import { NavigateFunction, useNavigate } from 'react-router-dom' +import classNames from 'classnames' +import html2canvas from 'html2canvas' + +import { + FacebookSocialShareBtn, + fileDownloadCanvasAsImage, + IconOutline, + LinkedinSocialShareBtn, + LoadingSpinner, + TwitterSocialShareBtn, + UserProfile, +} from '../../../../lib' +import { + TCACertificationProviderData, + UserCompletedTCACertificationsProviderData, + useGetUserTCACompletedCertificationsMOCK, + useGetTCACertificationMOCK, +} from '../../learn-lib' +import { getTCACertificationPath, getUserTCACertificateSsr } from '../../learn.routes' + +import { ActionButton } from './action-button' +import { Certificate } from './certificate' +import { useCertificateScaling } from './use-certificate-scaling.hook' +import styles from './CertificateView.module.scss' + +export type CertificateViewStyle = 'large-container' | undefined + +interface CertificateViewProps { + certification: string, + hideActions?: boolean, + onCertificationNotCompleted: () => void + profile: UserProfile, + viewStyle: CertificateViewStyle +} + +const CertificateView: FC = (props: CertificateViewProps) => { + + const navigate: NavigateFunction = useNavigate() + const tcaCertificationPath: string = getTCACertificationPath(props.certification) + const certificateElRef: MutableRefObject = useRef() + const certificateWrapRef: MutableRefObject = useRef() + + const userName: string = useMemo(() => ( + [props.profile.firstName, props.profile.lastName].filter(Boolean) + .join(' ') + || props.profile.handle + ), [props.profile.firstName, props.profile.handle, props.profile.lastName]) + + const { + certification, + ready: certReady, + }: TCACertificationProviderData = useGetTCACertificationMOCK(props.certification) + + function getCertTitle(user: string): string { + return `${user} - ${certification?.title} Certification` + } + + const certUrl: string = getUserTCACertificateSsr( + props.certification, + props.profile.handle, + getCertTitle(props.profile.handle), + ) + + const certificationTitle: string = getCertTitle(userName || props.profile.handle) + + const { + certifications: [completedCertificate], + ready: completedCertificateReady, + }: UserCompletedTCACertificationsProviderData = useGetUserTCACompletedCertificationsMOCK( + props.profile.userId, + props.certification, + ) + + const hasCompletedTheCertification: boolean = !!completedCertificate + + const ready: boolean = useMemo(() => ( + completedCertificateReady && certReady + ), [completedCertificateReady, certReady]) + + const readyAndCompletedCertification: boolean = useMemo(() => ( + ready && hasCompletedTheCertification + ), [hasCompletedTheCertification, ready]) + + useCertificateScaling(ready ? certificateWrapRef : undefined) + + const handleBackBtnClick: () => void = useCallback(() => { + navigate(tcaCertificationPath) + }, [tcaCertificationPath, navigate]) + + const getCertificateCanvas: () => Promise = useCallback(async () => { + + if (!certificateElRef.current) { + return undefined + } + + return html2canvas(certificateElRef.current, { + // when canvas iframe is ready, remove text gradients + // as they're not supported in html2canvas + onclone: (doc: Document) => { + [].forEach.call(doc.querySelectorAll('.grad'), (el: HTMLDivElement) => { + el.classList.remove('grad') + }) + }, + // scale (pixelRatio) doesn't matter for the final ceriticate, use 1 + scale: 1, + // use the same (ideal) window size when rendering the certificate + windowHeight: 700, + windowWidth: 1024, + }) + }, []) + + const handleDownload: () => Promise = useCallback(async () => { + + const canvas: HTMLCanvasElement | void = await getCertificateCanvas() + if (!!canvas) { + fileDownloadCanvasAsImage(canvas, `${certificationTitle}.png`) + } + + }, [certificationTitle, getCertificateCanvas]) + + const handlePrint: () => Promise = useCallback(async () => { + + const canvas: HTMLCanvasElement | void = await getCertificateCanvas() + if (!canvas) { + return + } + + const printWindow: Window | null = window.open('') + if (!printWindow) { + return + } + + printWindow.document.body.appendChild(canvas) + printWindow.document.title = certificationTitle + printWindow.focus() + printWindow.print() + }, [certificationTitle, getCertificateCanvas]) + + useEffect(() => { + if (ready && !hasCompletedTheCertification) { + props.onCertificationNotCompleted() + } + }, [tcaCertificationPath, hasCompletedTheCertification, props, ready]) + + return ( + <> + + + {ready && readyAndCompletedCertification && ( +
+
+ {!props.hideActions && ( +
+ } + onClick={handleBackBtnClick} + /> +
+ )} +
+ +
+ {!props.hideActions && ( +
+ } + onClick={handlePrint} + /> + } + onClick={handleDownload} + /> + + + +
+ )} +
+
+ )} + + ) +} + +export default CertificateView diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.module.scss b/src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.module.scss new file mode 100644 index 000000000..a80e1bf89 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.module.scss @@ -0,0 +1,21 @@ +@import '../../../../../lib/styles/includes'; + +.wrap { + @include icon-mxx; + border-radius: 50%; + + color: $tc-white; + border: $border solid $tc-white; + + display: flex; + align-items: center; + justify-content: center; + + padding: $space-sm; + + cursor: pointer; + + svg { + @include icon-xxl; + } +} diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.tsx b/src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.tsx new file mode 100644 index 000000000..b85d0918b --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.tsx @@ -0,0 +1,36 @@ +import { FC, ReactNode } from 'react' + +import styles from './ActionButton.module.scss' + +interface ActionButtonProps { + icon: ReactNode + onClick?: () => void + target?: string + url?: string +} + +const ActionButton: FC = (props: ActionButtonProps) => { + + // if there is a url, this is a link button + if (!!props.url) { + return ( + + {props.icon} + + ) + } + + return ( +
+ {props.icon} +
+ ) +} + +export default ActionButton diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/action-button/index.ts b/src-ts/tools/learn/tca-certificate/certificate-view/action-button/index.ts new file mode 100644 index 000000000..d1b1093a4 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/action-button/index.ts @@ -0,0 +1 @@ +export { default as ActionButton } from './ActionButton' diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/Certificate.module.scss b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/Certificate.module.scss new file mode 100644 index 000000000..1ea492c07 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/Certificate.module.scss @@ -0,0 +1,168 @@ +@import '../../../../../lib/styles/includes'; +@import './includes'; + +.wrap { + display: flex; + height: 100%; +} + +.details { + width: 55%; + padding: calc($space-mx + $space-lg); + display: flex; + flex-direction: column; + flex: 1; + + &-inner { + max-width: 385px; + flex: 1; + display: flex; + flex-direction: column; + } + + .wrap:global(.large-container) & { + padding-left: calc($space-mx + $space-mxx); + } + + h2 { + color: $blue-160; + &:global(.grad) { + @include grad-text-color($tc-grad15); + } + } + + h3 { + font-size: 48px; + line-height: 50px; + color: $tc-black; + margin-top: $space-sm; + } + + h1 { + font-size: 80px; + line-height: 72px; + } + + &:global(.theme-dev) { + .username { + color: $tc-dev-track-color; + } + .username:global(.grad) { + @include grad-text-color($tc-dev-grad); + } + .tc-handle { + color: $turq-140; + } + } + &:global(.theme-design) { + .username { + color: $tc-design-track-color; + } + .username:global(.grad) { + @include grad-text-color($tc-design-grad); + } + .tc-handle { + color: $blue-140; + } + } + &:global(.theme-qa) { + .username { + color: $tc-qa-track-color; + } + .username:global(.grad) { + @include grad-text-color($tc-qa-grad); + } + .tc-handle { + color: $purple-120; + } + } + &:global(.theme-datascience) { + .username { + color: $tc-datascience-track-color; + } + .username:global(.grad) { + @include grad-text-color($tc-datascience-grad); + } + .tc-handle { + color: $red-140; + } + } +} + +.username { + margin-top: calc($space-mx + $space-sm); +} + +.tc-handle { + color: $turq-140; + + margin-top: $space-xl; +} + +.badges { + width: 45%; + position: relative; + z-index: 1; + + padding: 56px; + padding-left: 80px; + + display: flex; + flex-direction: column; + max-width: 500px; +} + +.course-card { + margin: auto; +} + +.pattern-bg { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; +} + +.logos { + margin-top: auto; + display: flex; +} + +.logo { + display: flex; + align-items: center; + height: 52px; + + svg { + width: auto; + height: 100%; + } +} + +.divider { + width: $border; + background: $black-10; + margin: 0 $space-xxxxl; +} + +.vendor { + color: $tc-white; + text-align: right; + + :global(.body-ultra-small) { + line-height: 22px; + } +} + +.vendor-logo { + margin-top: $space-sm; + display: flex; + justify-content: flex-end; + + svg { + display: block; + height: 16px; + } +} diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/Certificate.tsx b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/Certificate.tsx new file mode 100644 index 000000000..db3b1e808 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/Certificate.tsx @@ -0,0 +1,86 @@ +import { FC, MutableRefObject } from 'react' +import classNames from 'classnames' + +import { LearnConfig } from '../../../learn-config' +import { LearnCertificateTrackType } from '../../../learn-lib' + +import { CertificateBgPattern } from './certificate-bg-pattern' +import { CourseCard } from './course-card' + +import styles from './Certificate.module.scss' +import { FccLogoSvg, TcAcademyLogoSvg, TcLogoSvg } from '../../../../../lib' + +interface CertificateProps { + completedDate?: string + course?: string + elRef?: MutableRefObject + provider?: string + tcHandle?: string + type?: LearnCertificateTrackType + userName?: string + viewStyle?: 'large-container' +} + +const Certificate: FC = (props: CertificateProps) => { + + const certificateType: LearnCertificateTrackType = props.type ?? 'DEV' + + const elementSelector: { [attr: string]: string } = { + [LearnConfig.CERT_ELEMENT_SELECTOR.attribute]: LearnConfig.CERT_ELEMENT_SELECTOR.value, + } + + return ( +
+
+
+

Topcoder Academy

+

Certificate of Completion

+

+ {props.userName} +

+
+ Topcoder Handle: + {props.tcHandle} +
+
+
+ +
+
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+ Course content provided by + {' '} + {props.provider} +
+
+ +
+
+
+
+ ) +} + +export default Certificate diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/CertificateBgPattern.module.scss b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/CertificateBgPattern.module.scss new file mode 100644 index 000000000..02c4c5bec --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/CertificateBgPattern.module.scss @@ -0,0 +1,58 @@ +@import '../includes'; + +@mixin wave-bg-pattern($grad) { + background: url('./wave-bg-2.png') 0 0 no-repeat, + url('./pattern-bg.png') bottom right no-repeat, + $grad; + background-size: 12px 100%, 220% 100%, 100% 100%; +} + +.wrap { + width: 100%; + height: 100%; + background: 0 0 no-repeat; + background-size: 100% 100%; + + position: relative; + + &:global(.theme-dev) { + background-image: $tc-dev-grad; + } + &:global(.theme-design) { + background-image: $tc-design-grad; + } + &:global(.theme-qa) { + background-image: $tc-qa-grad; + } + + &:global(.theme-datascience) { + background-image: $tc-datascience-grad; + } + + &:global(.theme-interview) { + background-image: $tc-interview-grad; + } + + &:global(.theme-security) { + background-image: $tc-security-grad; + } + + > div { + position: absolute; + top: 0; + left: -1px; + width: 100%; + height: 100%; + z-index: 1; + + &:global(.pattern-bg) { + background: url('./pattern-bg.png') bottom right no-repeat; + background-size: 220% 100%; + } + + &:global(.wave-bg) { + background: url('./wave-bg.png') 0 0 repeat-y; + background-size: 400px 116px; + } + } +} diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/CertificateBgPattern.tsx b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/CertificateBgPattern.tsx new file mode 100644 index 000000000..61c86af60 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/CertificateBgPattern.tsx @@ -0,0 +1,19 @@ +import { FC } from 'react' +import classNames from 'classnames' + +import { LearnCertificateTrackType } from '../../../../learn-lib' + +import styles from './CertificateBgPattern.module.scss' + +interface CertificateBgPatternProps { + type: LearnCertificateTrackType +} + +const CertificateBgPattern: FC = (props: CertificateBgPatternProps) => ( +
+
+
+
+) + +export default CertificateBgPattern diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/index.ts b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/index.ts new file mode 100644 index 000000000..68b6d47de --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/index.ts @@ -0,0 +1 @@ +export { default as CertificateBgPattern } from './CertificateBgPattern' diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/pattern-bg.png b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/pattern-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..dacb6a7eff1a64773af6369f6bf4f79a8db09d87 GIT binary patch literal 48910 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 literal 0 HcmV?d00001 diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/wave-bg.png b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/certificate-bg-pattern/wave-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..f7f87b7f2a1f52786bb40d9bb332dc9714d6d98f GIT binary patch literal 899 zcmeAS@N?(olHy`uVBq!ia0y~yU{(OKud*-$$=H{jIY5fBILO_J@#aaLdLW0hz$3Dl zfk8|agc&`9R6YO&r2~9IT>t<74`c#KOu!r9?E%tT666>BpP`bIgUMV%((jd!=TJh*$*k|U&4e7)-StJ$@` ze2=a@UNt|CZF=kPr3(b_eJFY)#6P0Vqg%mn zy~47pL)slH=WSp$yvn@|Xm+E5VYGmLx@eB0&sQeLq;CrHm%AVGJaTPsuOOJU)GJ<2D(BnhavPnfZ{{qlF9n>Qb2xD9g78}4MVUk;}Jv1~e0 z@v8UZ1W};H?Q0~AZJ%8|T)l+T4djgV3UvYg2A7l1e34KFIv+x$zc+OAI3s%G`vIk% z2@fTsM}-c1oUv>73t(<$2o+OTY$bwo-YTcA3snz=F<-f3kmf|L%B`h zfJytgJ-r)fH^>i#wM9L(i9O5j!2?ag{=qSeQ(cADIX#mnBSeur3<+Ekq>kCINcwZ! zAZ_jUm#P~+Z+`*Rnzr`+W$%RN#d?PCVtT;w_4M1!X;0rQIhy(h7&kiBho?PV_fbrq zwOdf-*mS$(8g6r7be~tqzuf&Gb>=JGrXzQnEaKioEUP-L+i`hKU$nsTNZJ34OSd0r Ul4;cH24)5ZPgg&ebxsLQ05}kDn*aa+ literal 0 HcmV?d00001 diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/CourseCard.module.scss b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/CourseCard.module.scss new file mode 100644 index 000000000..f15f09a95 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/CourseCard.module.scss @@ -0,0 +1,52 @@ +@import '../../../../../../lib/styles/includes'; + +.wrap { + border-radius: $space-sm; + width: 100%; + width: 264px; + background: rgba($tc-white, 0.12); + backdrop-filter: blur(160px); + color: $tc-white; + + h5 { + font-size: 24px; + line-height: 26px; + } +} + +.badge { + img { + display: block; + @include icon-size(78); + } +} + +.course-title { + margin-top: $space-lg; +} + +.top-wrap { + background: url(./wave-bg.png) 0 0 no-repeat; + background-size: 264px 600px; + padding: $space-xxl; + min-height: 264px; +} + +.details { + display: flex; + align-items: center; + padding: $space-xxl; + gap: calc($space-sm + $border); + + svg { + @include icon-xl; + } + + span { + line-height: 24px; + + & > span { + display: block; + } + } +} diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/CourseCard.tsx b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/CourseCard.tsx new file mode 100644 index 000000000..aa669788c --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/CourseCard.tsx @@ -0,0 +1,41 @@ +import { FC } from 'react' +import classNames from 'classnames' + +import { IconOutline, textFormatDateLocaleShortString } from '../../../../../../lib' +import { CourseBadge, LearnCertificateTrackType } from '../../../../learn-lib' + +import styles from './CourseCard.module.scss' + +interface CourseCardProps { + completedDate?: string + course?: string + type: LearnCertificateTrackType +} + +const CourseCard: FC = (props: CourseCardProps) => ( +
+
+
+ +
+
+ {props.course} +
+
+
+ + + Completed + + { + props.completedDate && ( + textFormatDateLocaleShortString(new Date(props.completedDate)) + ) + } + + +
+
+) + +export default CourseCard diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/index.ts b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/index.ts new file mode 100644 index 000000000..63d15ecfc --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/index.ts @@ -0,0 +1 @@ +export { default as CourseCard } from './CourseCard' diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/wave-bg.png b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/course-card/wave-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..c23dfb5fda4ac3db8e9448881d756aac0c020048 GIT binary patch literal 2399 zcmeAS@N?(olHy`uVBq!ia0y~yU=m29w)SM5b7>k44ofvPP)Tsw@I14-? ziy0WiR6&^0Gf3qFP|z&EC&U#<|0f-MOg{Dps8gsU$S;_IiAPvcR-Q#rnoU%kLx7iy z-+z_20s{lb5>FS$kcv5PZ@vzD}Zoa*IguQfg+?{jZ0%ds2Ze*U|VQ#da8uRZ+s&qXcxR^UK79DqWzNq>RmyLE3*gOHn_*y6 zQ|I>I%K9jDd@osl z)UWo~-YDDt+P`NjmzP|UzP!OETX=K(^!s+3CT#SS`K$cul4QD_?7d#^M|W1Z=Rf-S zhWAdt@1v&QXa%m{pxA?MuLgH_WyGN}ol71VU zZT*$Jb;s`?b0ns<|37MS^|k-B+sm2ja+lpXl=1oZ@vq;~jNkmLtuNjl*Yoq=50}-; zpYMK~cPMRhtlho(%WC=JyKCQ^EBvf`dq-_e_227^!S_t^nde eQ9#cS(EHABoqx4Yx;Z)-WUZ&GpUXO@geCw7;PILO literal 0 HcmV?d00001 diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/includes.scss b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/includes.scss new file mode 100644 index 000000000..6db0d3dd5 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/includes.scss @@ -0,0 +1,17 @@ +$tc-dev-track-color: #048467; +$tc-design-track-color: #065D6E; +$tc-qa-track-color: #363D8C; +$tc-datascience-track-color: #723390; +$tc-dev-grad: linear-gradient(84.92deg, #048467 2.08%, #064871 97.43%); +$tc-design-grad: linear-gradient(84.92deg, #065D6E 2.08%, #06596E 2.09%, #3E3B91 97.43%); +$tc-qa-grad: linear-gradient(84.92deg, #363D8C 2.08%, #723390 97.43%); +$tc-datascience-grad: linear-gradient(84.92deg, #723390 2.08%, #8C384F 97.43%); +$tc-interview-grad: linear-gradient(84.92deg, #048467 2.08%, #064871 33.85%, #6831A8 66.15%, #8C384D 97.43%); +$tc-security-grad: linear-gradient(84.92deg, #048467 2.08%, #064871 97.43%); + +@mixin grad-text-color($grad) { + background: $grad; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/certificate/index.ts b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/index.ts new file mode 100644 index 000000000..fd27dbdf7 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/certificate/index.ts @@ -0,0 +1 @@ +export { default as Certificate } from './Certificate' diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/index.ts b/src-ts/tools/learn/tca-certificate/certificate-view/index.ts new file mode 100644 index 000000000..37e2894f6 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/index.ts @@ -0,0 +1,2 @@ +export { default as CertificateView } from './CertificateView' +export type { CertificateViewStyle } from './CertificateView' diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/use-certificate-scaling.hook.tsx b/src-ts/tools/learn/tca-certificate/certificate-view/use-certificate-scaling.hook.tsx new file mode 100644 index 000000000..ada4cdac8 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/use-certificate-scaling.hook.tsx @@ -0,0 +1,27 @@ +import { MutableRefObject, useEffect } from 'react' + +export function useCertificateScaling( + certificateRef?: MutableRefObject, +): void { + + // the certificate isn't responsive: should look the same on mobile and desktop + // add resize event listener to downscale the certificate + useEffect(() => { + function handleResize(): void { + if (!certificateRef?.current) { + return + } + + const certificateEl: HTMLDivElement = certificateRef.current + const parentWidth: number = certificateEl.parentElement?.offsetWidth ?? 0 + // 975 and 1250 are the original container sizes, + // and we're dividing by that to get the needed zoom level + const ratioSize: number = window.innerWidth <= 745 ? 975 : 1250 + Object.assign(certificateEl.style, { zoom: Math.min(1, parentWidth / ratioSize) }) + } + + window.addEventListener('resize', handleResize, false) + handleResize() + return () => window.removeEventListener('resize', handleResize, false) + }, [certificateRef]) +} diff --git a/src-ts/tools/learn/tca-certificate/index.ts b/src-ts/tools/learn/tca-certificate/index.ts new file mode 100644 index 000000000..1772df7f1 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/index.ts @@ -0,0 +1,2 @@ +export * from './my-certificate' +export * from './user-certificate' diff --git a/src-ts/tools/learn/tca-certificate/my-certificate/MyTCACertificate.tsx b/src-ts/tools/learn/tca-certificate/my-certificate/MyTCACertificate.tsx new file mode 100644 index 000000000..418883c66 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/my-certificate/MyTCACertificate.tsx @@ -0,0 +1,45 @@ +import { FC, useCallback, useContext, useEffect } from 'react' +import { NavigateFunction, Params, useNavigate, useParams } from 'react-router-dom' + +import { + LoadingSpinner, + profileContext, + ProfileContextData, +} from '../../../../lib' +import { getTCACertificationPath } from '../../learn.routes' +import CertificateView from '../certificate-view/CertificateView' + +const MyTCACertificate: FC<{}> = () => { + const routeParams: Params = useParams() + const { profile, initialized: profileReady }: ProfileContextData = useContext(profileContext) + + const navigate: NavigateFunction = useNavigate() + const certificationParam: string = routeParams.certification ?? '' + const tcaCertificationPath: string = getTCACertificationPath(certificationParam) + + const navigateToCertification: () => void = useCallback(() => { + navigate(tcaCertificationPath) + }, [tcaCertificationPath, navigate]) + + useEffect(() => { + if (profileReady && !profile) { + navigateToCertification() + } + }, [profileReady, profile, navigateToCertification]) + + return ( + <> + + + {profileReady && profile && ( + + )} + + ) +} + +export default MyTCACertificate diff --git a/src-ts/tools/learn/tca-certificate/my-certificate/index.ts b/src-ts/tools/learn/tca-certificate/my-certificate/index.ts new file mode 100644 index 000000000..e6bcb7548 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/my-certificate/index.ts @@ -0,0 +1 @@ +export { default as MyTCACertificate } from './MyTCACertificate' diff --git a/src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.module.scss b/src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.module.scss new file mode 100644 index 000000000..06821e91a --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.module.scss @@ -0,0 +1,5 @@ +.full-screen-cert { + flex: 1 1 auto; + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.tsx b/src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.tsx new file mode 100644 index 000000000..6be68de30 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.tsx @@ -0,0 +1,73 @@ +import { Dispatch, FC, MutableRefObject, SetStateAction, useEffect, useLayoutEffect, useRef, useState } from 'react' +import { Params, useParams, useSearchParams } from 'react-router-dom' + +import { + LoadingSpinner, + profileGetPublicAsync, + UserProfile, +} from '../../../../lib' +import { getViewStyleParamKey } from '../../learn.routes' +import { CertificateView, CertificateViewStyle } from '../certificate-view' + +import styles from './UserCertificate.module.scss' + +const UserCertificate: FC<{}> = () => { + + const wrapElRef: MutableRefObject = useRef() + const routeParams: Params = useParams() + const [queryParams]: [URLSearchParams, any] = useSearchParams() + + const [profile, setProfile]: [ + UserProfile | undefined, + Dispatch> + ] = useState() + const [profileReady, setProfileReady]: [boolean, Dispatch>] = useState(false) + + const providerParam: string = routeParams.provider ?? '' + const certificationParam: string = routeParams.certification ?? '' + + useEffect(() => { + if (routeParams.memberHandle) { + profileGetPublicAsync(routeParams.memberHandle) + .then(userProfile => { + setProfile(userProfile) + setProfileReady(true) + }) + } + }, [routeParams.memberHandle, setProfileReady]) + + useLayoutEffect(() => { + const el: HTMLElement = wrapElRef.current + if (!el) { + return + } + + [].forEach.call(el.parentElement?.children ?? [], (c: HTMLElement) => { + if (c !== el) { + Object.assign(c.style, { display: 'none' }) + } + }) + el.classList.add(styles['full-screen-cert']) + }) + + return ( + <> + + + {profileReady && profile && ( +
+ { }} + hideActions + viewStyle={queryParams.get(getViewStyleParamKey()) as CertificateViewStyle} + /> +
+ )} + + ) +} + +export default UserCertificate diff --git a/src-ts/tools/learn/tca-certificate/user-certificate/index.ts b/src-ts/tools/learn/tca-certificate/user-certificate/index.ts new file mode 100644 index 000000000..a2cec5f44 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/user-certificate/index.ts @@ -0,0 +1 @@ +export { default as UserCertificate } from './UserCertificate' From 9df003ad29bd1a3ba8294b505ef5233fccec3bc7 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 23 Jan 2023 11:38:40 +0200 Subject: [PATCH 2/5] TCA-870 add the public user cert route --- ...-completed-tca-certifications.provider.tsx | 2 +- src-ts/tools/learn/learn.routes.tsx | 7 ++++++ ...le.scss => UserTCACertificate.module.scss} | 0 ...Certificate.tsx => UserTCACertificate.tsx} | 23 +++++++++++-------- .../tca-certificate/user-certificate/index.ts | 2 +- 5 files changed, 23 insertions(+), 11 deletions(-) rename src-ts/tools/learn/tca-certificate/user-certificate/{UserCertificate.module.scss => UserTCACertificate.module.scss} (100%) rename src-ts/tools/learn/tca-certificate/user-certificate/{UserCertificate.tsx => UserTCACertificate.tsx} (70%) diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certifications.provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certifications.provider.tsx index 80e6905d5..a7f5b6f6c 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certifications.provider.tsx +++ b/src-ts/tools/learn/learn-lib/data-providers/user-completed-tca-certifications-provider/user-completed-tca-certifications.provider.tsx @@ -6,7 +6,7 @@ import { UserCompletedTCACertification } from './user-completed-tca-certificatio import { UserCompletedTCACertificationsProviderData } from './user-completed-tca-certifications-provider-data.model' const COMPLETED_CERTS_MOCK = [ - { status: 'comleted', trackType: 'web dev', completedDate: '' }, + { status: 'comleted', trackType: 'web dev', completedDate: 'Dec 19, 2022' }, ] export function useGetUserTCACompletedCertifications( diff --git a/src-ts/tools/learn/learn.routes.tsx b/src-ts/tools/learn/learn.routes.tsx index 75f94fe67..40aa3407f 100644 --- a/src-ts/tools/learn/learn.routes.tsx +++ b/src-ts/tools/learn/learn.routes.tsx @@ -12,6 +12,7 @@ const FreeCodeCamp: LazyLoadedComponent = lazyLoad(() => import('./free-code-cam const MyLearning: LazyLoadedComponent = lazyLoad(() => import('./my-learning'), 'MyLearning') const LandingLearn: LazyLoadedComponent = lazyLoad(() => import('./Learn')) const MyTCACertificate: LazyLoadedComponent = lazyLoad(() => import('./tca-certificate'), 'MyTCACertificate') +const UserTCACertificate: LazyLoadedComponent = lazyLoad(() => import('./tca-certificate'), 'UserTCACertificate') export enum LEARN_PATHS { certificate = '/certificate', @@ -147,6 +148,12 @@ export const learnRoutes: ReadonlyArray = [ id: 'My TCA Certification', route: 'tca-certifications/:certification/certificate', }, + { + children: [], + element: , + id: 'User TCA Certification', + route: 'tca-certifications/:certification/:memberHandle/certificate', + }, ], element: , id: toolTitle, diff --git a/src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.module.scss b/src-ts/tools/learn/tca-certificate/user-certificate/UserTCACertificate.module.scss similarity index 100% rename from src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.module.scss rename to src-ts/tools/learn/tca-certificate/user-certificate/UserTCACertificate.module.scss diff --git a/src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.tsx b/src-ts/tools/learn/tca-certificate/user-certificate/UserTCACertificate.tsx similarity index 70% rename from src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.tsx rename to src-ts/tools/learn/tca-certificate/user-certificate/UserTCACertificate.tsx index 6be68de30..a7295e901 100644 --- a/src-ts/tools/learn/tca-certificate/user-certificate/UserCertificate.tsx +++ b/src-ts/tools/learn/tca-certificate/user-certificate/UserTCACertificate.tsx @@ -1,18 +1,19 @@ -import { Dispatch, FC, MutableRefObject, SetStateAction, useEffect, useLayoutEffect, useRef, useState } from 'react' -import { Params, useParams, useSearchParams } from 'react-router-dom' +import { Dispatch, FC, MutableRefObject, SetStateAction, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' +import { NavigateFunction, Params, useNavigate, useParams, useSearchParams } from 'react-router-dom' import { LoadingSpinner, profileGetPublicAsync, UserProfile, } from '../../../../lib' -import { getViewStyleParamKey } from '../../learn.routes' +import { getTCACertificationPath, getViewStyleParamKey } from '../../learn.routes' import { CertificateView, CertificateViewStyle } from '../certificate-view' -import styles from './UserCertificate.module.scss' +import styles from './UserTCACertificate.module.scss' -const UserCertificate: FC<{}> = () => { +const UserTCACertificate: FC<{}> = () => { + const navigate: NavigateFunction = useNavigate() const wrapElRef: MutableRefObject = useRef() const routeParams: Params = useParams() const [queryParams]: [URLSearchParams, any] = useSearchParams() @@ -23,9 +24,10 @@ const UserCertificate: FC<{}> = () => { ] = useState() const [profileReady, setProfileReady]: [boolean, Dispatch>] = useState(false) - const providerParam: string = routeParams.provider ?? '' const certificationParam: string = routeParams.certification ?? '' + const tcaCertificationPath: string = getTCACertificationPath(certificationParam) + useEffect(() => { if (routeParams.memberHandle) { profileGetPublicAsync(routeParams.memberHandle) @@ -50,6 +52,10 @@ const UserCertificate: FC<{}> = () => { el.classList.add(styles['full-screen-cert']) }) + const navigateToCertification: () => void = useCallback(() => { + navigate(tcaCertificationPath) + }, [tcaCertificationPath, navigate]) + return ( <> @@ -59,8 +65,7 @@ const UserCertificate: FC<{}> = () => { { }} + onCertificationNotCompleted={navigateToCertification} hideActions viewStyle={queryParams.get(getViewStyleParamKey()) as CertificateViewStyle} /> @@ -70,4 +75,4 @@ const UserCertificate: FC<{}> = () => { ) } -export default UserCertificate +export default UserTCACertificate diff --git a/src-ts/tools/learn/tca-certificate/user-certificate/index.ts b/src-ts/tools/learn/tca-certificate/user-certificate/index.ts index a2cec5f44..58bd97b52 100644 --- a/src-ts/tools/learn/tca-certificate/user-certificate/index.ts +++ b/src-ts/tools/learn/tca-certificate/user-certificate/index.ts @@ -1 +1 @@ -export { default as UserCertificate } from './UserCertificate' +export { default as UserTCACertificate } from './UserTCACertificate' From beb21186f961f6d45594bdda0c99f202930c9a4a Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 23 Jan 2023 12:11:15 +0200 Subject: [PATCH 3/5] TCA-870 update TCA certs model to latest per API --- .../tca-certification-category.model.ts | 7 +++++++ .../tca-certification.model.ts | 8 ++++++++ 2 files changed, 15 insertions(+) create mode 100644 src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-category.model.ts diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-category.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-category.model.ts new file mode 100644 index 000000000..e0e3add02 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-category.model.ts @@ -0,0 +1,7 @@ +export interface TCACertificationCategory { + id: number + category: string + track: string + createdAt: Date + updatedAt: Date +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification.model.ts index 8aab5a9ff..407ad9acd 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification.model.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification.model.ts @@ -1,15 +1,23 @@ import { TCACertificationLearnLevel } from './tca-certificate-level-type' import { TCACertificationStatus } from './tca-certificate-status-type' +import { TCACertificationCategory } from './tca-certification-category.model' export interface TCACertification { id: number title: string + dashedName: string description: string + introText: string estimatedCompletionTime: number status: TCACertificationStatus sequentialCourses: boolean learnerLevel: TCACertificationLearnLevel certificationCategoryId: string + certificationCategory: TCACertificationCategory stripeProductId?: string skills: string[] + learningOutcomes: string[] + prerequisites: string[] + createdAt: Date + updatedAt: Date } From 3423eab7c87232878df56e7ee2a749804981fe03 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Tue, 24 Jan 2023 13:10:09 +0200 Subject: [PATCH 4/5] TCA-870 reusable components --- .../certificate-view/CertificateView.tsx | 4 +-- .../action-button/ActionButton.module.scss | 2 +- .../action-button/ActionButton.tsx | 0 .../action-button/index.ts | 0 .../tca-certifications.provider.tsx | 5 ++- src-ts/tools/learn/learn-lib/index.ts | 2 ++ .../use-certificate-scaling-hook/index.ts | 1 + .../useCertificateScaling.hook.tsx} | 0 .../certificate-view/CertificateView.tsx | 8 ++--- .../action-button/ActionButton.module.scss | 21 ----------- .../action-button/ActionButton.tsx | 36 ------------------- .../certificate-view/action-button/index.ts | 1 - .../use-certificate-scaling.hook.tsx | 27 -------------- 13 files changed, 14 insertions(+), 93 deletions(-) rename src-ts/tools/learn/{course-certificate/certificate-view => learn-lib}/action-button/ActionButton.module.scss (86%) rename src-ts/tools/learn/{course-certificate/certificate-view => learn-lib}/action-button/ActionButton.tsx (100%) rename src-ts/tools/learn/{course-certificate/certificate-view => learn-lib}/action-button/index.ts (100%) create mode 100644 src-ts/tools/learn/learn-lib/use-certificate-scaling-hook/index.ts rename src-ts/tools/learn/{course-certificate/certificate-view/use-certificate-scaling.hook.tsx => learn-lib/use-certificate-scaling-hook/useCertificateScaling.hook.tsx} (100%) delete mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.module.scss delete mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.tsx delete mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/action-button/index.ts delete mode 100644 src-ts/tools/learn/tca-certificate/certificate-view/use-certificate-scaling.hook.tsx diff --git a/src-ts/tools/learn/course-certificate/certificate-view/CertificateView.tsx b/src-ts/tools/learn/course-certificate/certificate-view/CertificateView.tsx index 9e6e1d428..4958e57c9 100644 --- a/src-ts/tools/learn/course-certificate/certificate-view/CertificateView.tsx +++ b/src-ts/tools/learn/course-certificate/certificate-view/CertificateView.tsx @@ -13,8 +13,10 @@ import { UserProfile, } from '../../../../lib' import { + ActionButton, AllCertificationsProviderData, CoursesProviderData, + useCertificateScaling, useGetCertification, useGetCourses, useGetUserCompletedCertifications, @@ -22,9 +24,7 @@ import { } from '../../learn-lib' import { getCoursePath, getUserCertificateSsr } from '../../learn.routes' -import { ActionButton } from './action-button' import { Certificate } from './certificate' -import { useCertificateScaling } from './use-certificate-scaling.hook' import styles from './CertificateView.module.scss' export type CertificateViewStyle = 'large-container' | undefined diff --git a/src-ts/tools/learn/course-certificate/certificate-view/action-button/ActionButton.module.scss b/src-ts/tools/learn/learn-lib/action-button/ActionButton.module.scss similarity index 86% rename from src-ts/tools/learn/course-certificate/certificate-view/action-button/ActionButton.module.scss rename to src-ts/tools/learn/learn-lib/action-button/ActionButton.module.scss index a80e1bf89..311d9cffb 100644 --- a/src-ts/tools/learn/course-certificate/certificate-view/action-button/ActionButton.module.scss +++ b/src-ts/tools/learn/learn-lib/action-button/ActionButton.module.scss @@ -1,4 +1,4 @@ -@import '../../../../../lib/styles/includes'; +@import '../../../../lib/styles/includes'; .wrap { @include icon-mxx; diff --git a/src-ts/tools/learn/course-certificate/certificate-view/action-button/ActionButton.tsx b/src-ts/tools/learn/learn-lib/action-button/ActionButton.tsx similarity index 100% rename from src-ts/tools/learn/course-certificate/certificate-view/action-button/ActionButton.tsx rename to src-ts/tools/learn/learn-lib/action-button/ActionButton.tsx diff --git a/src-ts/tools/learn/course-certificate/certificate-view/action-button/index.ts b/src-ts/tools/learn/learn-lib/action-button/index.ts similarity index 100% rename from src-ts/tools/learn/course-certificate/certificate-view/action-button/index.ts rename to src-ts/tools/learn/learn-lib/action-button/index.ts diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certifications.provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certifications.provider.tsx index d5894f186..a10b889cc 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certifications.provider.tsx +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certifications.provider.tsx @@ -9,6 +9,7 @@ import { useSwrCache } from '../../learn-swr' import { TCACertificationProviderData, TCACertificationsProviderData } from './tca-certifications-provider-data.model' import { TCACertification } from './tca-certification.model' +import { find } from 'lodash' interface TCACertificationsAllProviderOptions { enabled?: boolean @@ -17,6 +18,7 @@ interface TCACertificationsAllProviderOptions { const TCACertificationMock: TCACertification[] = [{ id: 1, title: 'Web Development Fundamentals', + dashedName: 'web-development-fundamentals', description: 'The Web Developer Fundamentals certification will teach you the basics of HTML, CSS, javascript, front end libraries and will also introduce you to backend development.', estimatedCompletionTime: 4, learnerLevel: 'Beginner', @@ -28,6 +30,7 @@ const TCACertificationMock: TCACertification[] = [{ { id: 2, title: 'Data Science Fundamentals', + dashedName: 'data-science-fundamentals', description: 'The Data Science Fundamentals certification will teach you the basics of scientific computing, Data Analysis and machine learning while using Python. Additionally, you will learn about data visualization.', estimatedCompletionTime: 14, status: 'active', @@ -88,7 +91,7 @@ export function useGetTCACertificationMOCK( certification: string, ): TCACertificationProviderData { - const data: TCACertification = TCACertificationMock[certification as any] + const data: TCACertification | undefined = find(TCACertificationMock, { dashedName: certification }) return { certification: data, diff --git a/src-ts/tools/learn/learn-lib/index.ts b/src-ts/tools/learn/learn-lib/index.ts index 6a21a12f8..97a1dc221 100755 --- a/src-ts/tools/learn/learn-lib/index.ts +++ b/src-ts/tools/learn/learn-lib/index.ts @@ -1,3 +1,4 @@ +export * from './action-button' export * from './collapsible-pane' export * from './course-badge' export * from './course-outline' @@ -8,4 +9,5 @@ export * from './learn-breadcrumb-provider' export * from './learn-swr' export * from './my-course-card' export * from './svgs' +export * from './use-certificate-scaling-hook' export * from './wave-hero' diff --git a/src-ts/tools/learn/learn-lib/use-certificate-scaling-hook/index.ts b/src-ts/tools/learn/learn-lib/use-certificate-scaling-hook/index.ts new file mode 100644 index 000000000..0a724eaa0 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/use-certificate-scaling-hook/index.ts @@ -0,0 +1 @@ +export * from './useCertificateScaling.hook' diff --git a/src-ts/tools/learn/course-certificate/certificate-view/use-certificate-scaling.hook.tsx b/src-ts/tools/learn/learn-lib/use-certificate-scaling-hook/useCertificateScaling.hook.tsx similarity index 100% rename from src-ts/tools/learn/course-certificate/certificate-view/use-certificate-scaling.hook.tsx rename to src-ts/tools/learn/learn-lib/use-certificate-scaling-hook/useCertificateScaling.hook.tsx diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx b/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx index 12d2ecb13..8d7bea337 100644 --- a/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx +++ b/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx @@ -13,16 +13,16 @@ import { UserProfile, } from '../../../../lib' import { + ActionButton, TCACertificationProviderData, - UserCompletedTCACertificationsProviderData, - useGetUserTCACompletedCertificationsMOCK, + useCertificateScaling, useGetTCACertificationMOCK, + useGetUserTCACompletedCertificationsMOCK, + UserCompletedTCACertificationsProviderData, } from '../../learn-lib' import { getTCACertificationPath, getUserTCACertificateSsr } from '../../learn.routes' -import { ActionButton } from './action-button' import { Certificate } from './certificate' -import { useCertificateScaling } from './use-certificate-scaling.hook' import styles from './CertificateView.module.scss' export type CertificateViewStyle = 'large-container' | undefined diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.module.scss b/src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.module.scss deleted file mode 100644 index a80e1bf89..000000000 --- a/src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.module.scss +++ /dev/null @@ -1,21 +0,0 @@ -@import '../../../../../lib/styles/includes'; - -.wrap { - @include icon-mxx; - border-radius: 50%; - - color: $tc-white; - border: $border solid $tc-white; - - display: flex; - align-items: center; - justify-content: center; - - padding: $space-sm; - - cursor: pointer; - - svg { - @include icon-xxl; - } -} diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.tsx b/src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.tsx deleted file mode 100644 index b85d0918b..000000000 --- a/src-ts/tools/learn/tca-certificate/certificate-view/action-button/ActionButton.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { FC, ReactNode } from 'react' - -import styles from './ActionButton.module.scss' - -interface ActionButtonProps { - icon: ReactNode - onClick?: () => void - target?: string - url?: string -} - -const ActionButton: FC = (props: ActionButtonProps) => { - - // if there is a url, this is a link button - if (!!props.url) { - return ( - - {props.icon} - - ) - } - - return ( -
- {props.icon} -
- ) -} - -export default ActionButton diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/action-button/index.ts b/src-ts/tools/learn/tca-certificate/certificate-view/action-button/index.ts deleted file mode 100644 index d1b1093a4..000000000 --- a/src-ts/tools/learn/tca-certificate/certificate-view/action-button/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ActionButton } from './ActionButton' diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/use-certificate-scaling.hook.tsx b/src-ts/tools/learn/tca-certificate/certificate-view/use-certificate-scaling.hook.tsx deleted file mode 100644 index ada4cdac8..000000000 --- a/src-ts/tools/learn/tca-certificate/certificate-view/use-certificate-scaling.hook.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { MutableRefObject, useEffect } from 'react' - -export function useCertificateScaling( - certificateRef?: MutableRefObject, -): void { - - // the certificate isn't responsive: should look the same on mobile and desktop - // add resize event listener to downscale the certificate - useEffect(() => { - function handleResize(): void { - if (!certificateRef?.current) { - return - } - - const certificateEl: HTMLDivElement = certificateRef.current - const parentWidth: number = certificateEl.parentElement?.offsetWidth ?? 0 - // 975 and 1250 are the original container sizes, - // and we're dividing by that to get the needed zoom level - const ratioSize: number = window.innerWidth <= 745 ? 975 : 1250 - Object.assign(certificateEl.style, { zoom: Math.min(1, parentWidth / ratioSize) }) - } - - window.addEventListener('resize', handleResize, false) - handleResize() - return () => window.removeEventListener('resize', handleResize, false) - }, [certificateRef]) -} From 107260be116dda359e501b50f6926c0c1921e0af Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Wed, 25 Jan 2023 09:12:10 +0200 Subject: [PATCH 5/5] TCA-870 extract canvas and print renderers to hooks --- .../certificate-view/CertificateView.tsx | 43 ++----------------- src-ts/tools/learn/learn-lib/index.ts | 2 + .../use-certificate-canvas-hook/index.ts | 1 + .../useCertificateCanvas.hook.tsx | 30 +++++++++++++ .../use-certificate-print-hook/index.ts | 1 + .../useCertificatePrint.hook.tsx | 29 +++++++++++++ .../certificate-view/CertificateView.tsx | 43 ++----------------- 7 files changed, 71 insertions(+), 78 deletions(-) create mode 100644 src-ts/tools/learn/learn-lib/use-certificate-canvas-hook/index.ts create mode 100644 src-ts/tools/learn/learn-lib/use-certificate-canvas-hook/useCertificateCanvas.hook.tsx create mode 100644 src-ts/tools/learn/learn-lib/use-certificate-print-hook/index.ts create mode 100644 src-ts/tools/learn/learn-lib/use-certificate-print-hook/useCertificatePrint.hook.tsx diff --git a/src-ts/tools/learn/course-certificate/certificate-view/CertificateView.tsx b/src-ts/tools/learn/course-certificate/certificate-view/CertificateView.tsx index 4958e57c9..fbb191f6b 100644 --- a/src-ts/tools/learn/course-certificate/certificate-view/CertificateView.tsx +++ b/src-ts/tools/learn/course-certificate/certificate-view/CertificateView.tsx @@ -1,7 +1,6 @@ import { FC, MutableRefObject, useCallback, useEffect, useMemo, useRef } from 'react' import { NavigateFunction, useNavigate } from 'react-router-dom' import classNames from 'classnames' -import html2canvas from 'html2canvas' import { FacebookSocialShareBtn, @@ -16,6 +15,8 @@ import { ActionButton, AllCertificationsProviderData, CoursesProviderData, + useCertificateCanvas, + useCertificatePrint, useCertificateScaling, useGetCertification, useGetCourses, @@ -102,27 +103,7 @@ const CertificateView: FC = (props: CertificateViewProps) navigate(coursePath) }, [coursePath, navigate]) - const getCertificateCanvas: () => Promise = useCallback(async () => { - - if (!certificateElRef.current) { - return undefined - } - - return html2canvas(certificateElRef.current, { - // when canvas iframe is ready, remove text gradients - // as they're not supported in html2canvas - onclone: (doc: Document) => { - [].forEach.call(doc.querySelectorAll('.grad'), (el: HTMLDivElement) => { - el.classList.remove('grad') - }) - }, - // scale (pixelRatio) doesn't matter for the final ceriticate, use 1 - scale: 1, - // use the same (ideal) window size when rendering the certificate - windowHeight: 700, - windowWidth: 1024, - }) - }, []) + const getCertificateCanvas: () => Promise = useCertificateCanvas(certificateElRef) const handleDownload: () => Promise = useCallback(async () => { @@ -133,23 +114,7 @@ const CertificateView: FC = (props: CertificateViewProps) }, [certificationTitle, getCertificateCanvas]) - const handlePrint: () => Promise = useCallback(async () => { - - const canvas: HTMLCanvasElement | void = await getCertificateCanvas() - if (!canvas) { - return - } - - const printWindow: Window | null = window.open('') - if (!printWindow) { - return - } - - printWindow.document.body.appendChild(canvas) - printWindow.document.title = certificationTitle - printWindow.focus() - printWindow.print() - }, [certificationTitle, getCertificateCanvas]) + const handlePrint: () => Promise = useCertificatePrint(certificateElRef, certificationTitle) useEffect(() => { if (ready && !hasCompletedTheCertification) { diff --git a/src-ts/tools/learn/learn-lib/index.ts b/src-ts/tools/learn/learn-lib/index.ts index f67a4c78a..b72d49eb0 100755 --- a/src-ts/tools/learn/learn-lib/index.ts +++ b/src-ts/tools/learn/learn-lib/index.ts @@ -10,5 +10,7 @@ export * from './learn-level-icon' export * from './learn-swr' export * from './my-course-card' export * from './svgs' +export * from './use-certificate-canvas-hook' +export * from './use-certificate-print-hook' export * from './use-certificate-scaling-hook' export * from './wave-hero' diff --git a/src-ts/tools/learn/learn-lib/use-certificate-canvas-hook/index.ts b/src-ts/tools/learn/learn-lib/use-certificate-canvas-hook/index.ts new file mode 100644 index 000000000..76a886856 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/use-certificate-canvas-hook/index.ts @@ -0,0 +1 @@ +export * from './useCertificateCanvas.hook' diff --git a/src-ts/tools/learn/learn-lib/use-certificate-canvas-hook/useCertificateCanvas.hook.tsx b/src-ts/tools/learn/learn-lib/use-certificate-canvas-hook/useCertificateCanvas.hook.tsx new file mode 100644 index 000000000..38536237e --- /dev/null +++ b/src-ts/tools/learn/learn-lib/use-certificate-canvas-hook/useCertificateCanvas.hook.tsx @@ -0,0 +1,30 @@ +import { MutableRefObject, useCallback } from 'react' +import html2canvas from 'html2canvas' + +export function useCertificateCanvas( + certificateElRef: MutableRefObject, +): () => Promise { + const getCertificateCanvas: () => Promise = useCallback(async () => { + + if (!certificateElRef.current) { + return undefined + } + + return html2canvas(certificateElRef.current, { + // when canvas iframe is ready, remove text gradients + // as they're not supported in html2canvas + onclone: (doc: Document) => { + [].forEach.call(doc.querySelectorAll('.grad'), (el: HTMLDivElement) => { + el.classList.remove('grad') + }) + }, + // scale (pixelRatio) doesn't matter for the final ceriticate, use 1 + scale: 1, + // use the same (ideal) window size when rendering the certificate + windowHeight: 700, + windowWidth: 1024, + }) + }, [certificateElRef]) + + return getCertificateCanvas +} diff --git a/src-ts/tools/learn/learn-lib/use-certificate-print-hook/index.ts b/src-ts/tools/learn/learn-lib/use-certificate-print-hook/index.ts new file mode 100644 index 000000000..62bbff159 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/use-certificate-print-hook/index.ts @@ -0,0 +1 @@ +export * from './useCertificatePrint.hook' diff --git a/src-ts/tools/learn/learn-lib/use-certificate-print-hook/useCertificatePrint.hook.tsx b/src-ts/tools/learn/learn-lib/use-certificate-print-hook/useCertificatePrint.hook.tsx new file mode 100644 index 000000000..22a2f7d73 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/use-certificate-print-hook/useCertificatePrint.hook.tsx @@ -0,0 +1,29 @@ +import { MutableRefObject, useCallback } from 'react' +import { useCertificateCanvas } from '../use-certificate-canvas-hook' + +export function useCertificatePrint( + certificateElRef: MutableRefObject, + certificationTitle: string, +): () => Promise { + const getCertificateCanvas: () => Promise = useCertificateCanvas(certificateElRef) + + const handlePrint: () => Promise = useCallback(async () => { + + const canvas: HTMLCanvasElement | void = await getCertificateCanvas() + if (!canvas) { + return + } + + const printWindow: Window | null = window.open('') + if (!printWindow) { + return + } + + printWindow.document.body.appendChild(canvas) + printWindow.document.title = certificationTitle + printWindow.focus() + printWindow.print() + }, [certificationTitle, getCertificateCanvas]) + + return handlePrint +} diff --git a/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx b/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx index 8d7bea337..5a0802bf7 100644 --- a/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx +++ b/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx @@ -1,7 +1,6 @@ import { FC, MutableRefObject, useCallback, useEffect, useMemo, useRef } from 'react' import { NavigateFunction, useNavigate } from 'react-router-dom' import classNames from 'classnames' -import html2canvas from 'html2canvas' import { FacebookSocialShareBtn, @@ -15,6 +14,8 @@ import { import { ActionButton, TCACertificationProviderData, + useCertificateCanvas, + useCertificatePrint, useCertificateScaling, useGetTCACertificationMOCK, useGetUserTCACompletedCertificationsMOCK, @@ -89,27 +90,7 @@ const CertificateView: FC = (props: CertificateViewProps) navigate(tcaCertificationPath) }, [tcaCertificationPath, navigate]) - const getCertificateCanvas: () => Promise = useCallback(async () => { - - if (!certificateElRef.current) { - return undefined - } - - return html2canvas(certificateElRef.current, { - // when canvas iframe is ready, remove text gradients - // as they're not supported in html2canvas - onclone: (doc: Document) => { - [].forEach.call(doc.querySelectorAll('.grad'), (el: HTMLDivElement) => { - el.classList.remove('grad') - }) - }, - // scale (pixelRatio) doesn't matter for the final ceriticate, use 1 - scale: 1, - // use the same (ideal) window size when rendering the certificate - windowHeight: 700, - windowWidth: 1024, - }) - }, []) + const getCertificateCanvas: () => Promise = useCertificateCanvas(certificateElRef) const handleDownload: () => Promise = useCallback(async () => { @@ -120,23 +101,7 @@ const CertificateView: FC = (props: CertificateViewProps) }, [certificationTitle, getCertificateCanvas]) - const handlePrint: () => Promise = useCallback(async () => { - - const canvas: HTMLCanvasElement | void = await getCertificateCanvas() - if (!canvas) { - return - } - - const printWindow: Window | null = window.open('') - if (!printWindow) { - return - } - - printWindow.document.body.appendChild(canvas) - printWindow.document.title = certificationTitle - printWindow.focus() - printWindow.print() - }, [certificationTitle, getCertificateCanvas]) + const handlePrint: () => Promise = useCertificatePrint(certificateElRef, certificationTitle) useEffect(() => { if (ready && !hasCompletedTheCertification) {