diff --git a/README.md b/README.md index 99e484165..044bb5e22 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,12 @@ By overriding the app to use port 443, you can use the authorized URL and >**NOTE:** Mac users will require running the app with elevated permissions in order to use a port lower than 500. +Easy way to overcome elevated permissions is to make use of: + +``` +sudo setcap 'cap_net_bind_service=+ep' `which node` +``` + For easier development, it is recommended that you add this certificate to your trusted root authorities and as a trused cert in your browser. Google your browser and OS for more info on how to trust cert authorities. Otherwise, you will need to override the exception each time you load the site. Firefox users may need to user an incognito browser in order to override the exception. diff --git a/package.json b/package.json index a880544cc..2cada7253 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@topcoder-platform/platform-ui", - "version": "2.0.7", + "version": "3.0.0", "private": true, "scripts": { "dev": "yarn react-app-rewired start", @@ -42,6 +42,7 @@ "moment": "^2.29.4", "moment-timezone": "^0.5.37", "prop-types": "^15.8.1", + "qrcode.react": "^3.1.0", "qs": "^6.11.0", "rc-checkbox": "^2.3.2", "react": "^18.2.0", diff --git a/src-ts/.eslintrc.js b/src-ts/.eslintrc.js index 10a2a1fb5..84fb4823e 100644 --- a/src-ts/.eslintrc.js +++ b/src-ts/.eslintrc.js @@ -73,7 +73,7 @@ module.exports = { ], 'complexity': [ 'error', - 11 + 14 ], 'import/extensions': 'off', 'import/no-named-default': 'off', diff --git a/src-ts/App.tsx b/src-ts/App.tsx index 10e9900f5..a075e7d2d 100644 --- a/src-ts/App.tsx +++ b/src-ts/App.tsx @@ -3,7 +3,7 @@ import { Routes } from 'react-router-dom' import { toast, ToastContainer } from 'react-toastify' import { Header } from './header' -import { routeContext, RouteContextData } from './lib' +import { routeContext, RouteContextData, useViewportUnitsFix } from './lib' const App: FC<{}> = () => { @@ -12,6 +12,8 @@ const App: FC<{}> = () => { const routeElements: Array = allRoutes .map(route => getRouteElement(route)) + useViewportUnitsFix() + return ( <>
diff --git a/src-ts/config/environments/environment.default.config.ts b/src-ts/config/environments/environment.default.config.ts index 525463eda..e04031166 100644 --- a/src-ts/config/environments/environment.default.config.ts +++ b/src-ts/config/environments/environment.default.config.ts @@ -2,6 +2,7 @@ import { EnvironmentConfigModel } from './environment-config.model' const COMMUNITY_WEBSITE: string = 'https://www.topcoder-dev.com' +const TCA_WEBSITE: string = 'https://platform-ui.topcoder-dev.com' export const EnvironmentConfigDefault: EnvironmentConfigModel = { ANALYTICS: { @@ -36,11 +37,13 @@ export const EnvironmentConfigDefault: EnvironmentConfigModel = { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.jl6Lp_friVNwEP8nfsfmL-vrQFzOFp2IfM_HC7AwGcg', }, TOPCODER_URLS: { + ACCOUNT_PROFILE: `${COMMUNITY_WEBSITE}/settings/profile`, ACCOUNT_SETTINGS: `${COMMUNITY_WEBSITE}/settings/account`, API_BASE: `${COMMUNITY_WEBSITE}/api`, BLOG_PAGE: `${COMMUNITY_WEBSITE}/blog`, CHALLENGES_PAGE: `${COMMUNITY_WEBSITE}/challenges`, GIGS_PAGE: `${COMMUNITY_WEBSITE}/gigs`, + TCA: `${TCA_WEBSITE}`, THRIVE_PAGE: `${COMMUNITY_WEBSITE}/thrive`, USER_PROFILE: `${COMMUNITY_WEBSITE}/members`, WP_CONTENT: `${COMMUNITY_WEBSITE}/wp-content`, diff --git a/src-ts/config/environments/environment.prod.config.ts b/src-ts/config/environments/environment.prod.config.ts index 222edbb03..a3968a309 100644 --- a/src-ts/config/environments/environment.prod.config.ts +++ b/src-ts/config/environments/environment.prod.config.ts @@ -3,6 +3,7 @@ import { EnvironmentConfigModel } from './environment-config.model' import { EnvironmentConfigDefault } from './environment.default.config' const COMMUNITY_WEBSITE: string = 'https://www.topcoder.com' +const TCA_WEBSITE: string = 'https://platform-ui.topcoder.com' export const EnvironmentConfigProd: EnvironmentConfigModel = { ...EnvironmentConfigDefault, @@ -34,11 +35,13 @@ export const EnvironmentConfigProd: EnvironmentConfigModel = { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.jl6Lp_friVNwEP8nfsfmL-vrQFzOFp2IfM_HC7AwGcg', }, TOPCODER_URLS: { + ACCOUNT_PROFILE: `${COMMUNITY_WEBSITE}/settings/profile`, ACCOUNT_SETTINGS: `${COMMUNITY_WEBSITE}/settings/account`, API_BASE: `${COMMUNITY_WEBSITE}/api`, BLOG_PAGE: `${COMMUNITY_WEBSITE}/blog`, CHALLENGES_PAGE: `${COMMUNITY_WEBSITE}/challenges`, GIGS_PAGE: `${COMMUNITY_WEBSITE}/gigs`, + TCA: `${TCA_WEBSITE}`, THRIVE_PAGE: `${COMMUNITY_WEBSITE}/thrive`, USER_PROFILE: `${COMMUNITY_WEBSITE}/members`, WP_CONTENT: `${COMMUNITY_WEBSITE}/wp-content`, diff --git a/src-ts/header/Header.tsx b/src-ts/header/Header.tsx index 9a9e37a04..5b06f1d05 100644 --- a/src-ts/header/Header.tsx +++ b/src-ts/header/Header.tsx @@ -145,7 +145,6 @@ const Header: FC = () => { return ( <> -
( + + + + + + + + + + + + + + +) + +export default DefaultMemberIcon diff --git a/src-ts/lib/default-member-icon/index.ts b/src-ts/lib/default-member-icon/index.ts new file mode 100644 index 000000000..271cb6f18 --- /dev/null +++ b/src-ts/lib/default-member-icon/index.ts @@ -0,0 +1 @@ +export { default as DefaultMemberIcon } from './DefaultMemberIcon' diff --git a/src-ts/lib/form/Form.tsx b/src-ts/lib/form/Form.tsx index c5b954062..2b9dd1514 100644 --- a/src-ts/lib/form/Form.tsx +++ b/src-ts/lib/form/Form.tsx @@ -4,6 +4,7 @@ import { Dispatch, FocusEvent, FormEvent, + ReactNode, RefObject, SetStateAction, useEffect, @@ -31,6 +32,7 @@ import styles from './Form.module.scss' interface FormProps { readonly action?: FormAction // only type submit will perform validation + readonly children?: ReactNode readonly formDef: FormDefinition readonly formValues?: ValueType readonly onChange?: (inputs: ReadonlyArray) => void, @@ -219,6 +221,8 @@ const Form: (props: onChange={onChange} /> + {props.children} +
{!!formError && (
(props: {formError}
)} -
+
{secondaryGroupButtons}
diff --git a/src-ts/lib/form/form-definition.model.ts b/src-ts/lib/form/form-definition.model.ts index efd292f8f..cd96082b5 100644 --- a/src-ts/lib/form/form-definition.model.ts +++ b/src-ts/lib/form/form-definition.model.ts @@ -13,7 +13,7 @@ export interface FormDefinition { readonly groupsOptions?: FormGroupOptions readonly shortName?: string readonly subtitle?: string - readonly successMessage?: string + readonly successMessage?: string | boolean readonly tabIndexStart?: number readonly title?: string } diff --git a/src-ts/lib/form/form-functions/form.functions.ts b/src-ts/lib/form/form-functions/form.functions.ts index f8e089601..62c63df78 100644 --- a/src-ts/lib/form/form-functions/form.functions.ts +++ b/src-ts/lib/form/form-functions/form.functions.ts @@ -122,10 +122,13 @@ export async function onSubmitAsync( return savePromise .then(() => { - const safeSuccessMessage: string = !!successMessage - ? successMessage as string - : `Your ${shortName || 'data'} has been saved.` - toast.success(safeSuccessMessage) + if (successMessage !== false) { + const safeSuccessMessage: string = !!successMessage + ? successMessage as string + : `Your ${shortName || 'data'} has been saved.` + toast.success(safeSuccessMessage) + } + onSuccess?.() }) .catch(error => Promise.reject(error.response?.data?.result?.content ?? error.message ?? error)) diff --git a/src-ts/lib/form/form-groups/form-input/input-text/InputText.tsx b/src-ts/lib/form/form-groups/form-input/input-text/InputText.tsx index 23e1d2303..929cbe4c8 100644 --- a/src-ts/lib/form/form-groups/form-input/input-text/InputText.tsx +++ b/src-ts/lib/form/form-groups/form-input/input-text/InputText.tsx @@ -23,6 +23,7 @@ export interface InputTextProps { readonly onBlur?: (event: FocusEvent) => void readonly onChange: (event: FocusEvent) => void readonly placeholder?: string + readonly readonly?: boolean readonly spellCheck?: boolean readonly tabIndex: number readonly type: InputTextTypes @@ -53,6 +54,7 @@ const InputText: FC = (props: InputTextProps) => { onChange={props.onChange} name={props.name} placeholder={props.placeholder} + readOnly={props.readonly} spellCheck={!!props.spellCheck} tabIndex={props.tabIndex} type={props.type || 'text'} diff --git a/src-ts/lib/form/form-input.model.ts b/src-ts/lib/form/form-input.model.ts index 313b72e40..d79e6fc18 100644 --- a/src-ts/lib/form/form-input.model.ts +++ b/src-ts/lib/form/form-input.model.ts @@ -44,6 +44,7 @@ export interface FormInputModel { readonly notTabbable?: boolean options?: ReadonlyArray readonly placeholder?: string + readonly readonly?: boolean readonly spellCheck?: boolean readonly title?: string touched?: boolean diff --git a/src-ts/lib/generic-page-moved/GenericPageMoved.module.scss b/src-ts/lib/generic-page-moved/GenericPageMoved.module.scss new file mode 100644 index 000000000..1d15a8e2d --- /dev/null +++ b/src-ts/lib/generic-page-moved/GenericPageMoved.module.scss @@ -0,0 +1,11 @@ + .wrap { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + h3 { + margin-bottom: 32px; + } +} diff --git a/src-ts/lib/generic-page-moved/GenericPageMoved.tsx b/src-ts/lib/generic-page-moved/GenericPageMoved.tsx new file mode 100644 index 000000000..6534ec988 --- /dev/null +++ b/src-ts/lib/generic-page-moved/GenericPageMoved.tsx @@ -0,0 +1,35 @@ +import { FC, useEffect } from 'react' + +import { Button } from '../button' + +import styles from './GenericPageMoved.module.scss' + +interface GenericPageMovedProps { + pageTitle: string + newPageUrl: string +} + +const GenericPageMoved: FC = (props: GenericPageMovedProps) => { + + // setup auto redirect in 5sec. + useEffect(() => { + const to: ReturnType = setTimeout(() => { + window.location.href = props.newPageUrl + }, 5000) + + return () => clearTimeout(to) + }, [props.newPageUrl]) + + return ( +
+

This page has moved.

+
+ ) +} + +export default GenericPageMoved diff --git a/src-ts/lib/generic-page-moved/index.ts b/src-ts/lib/generic-page-moved/index.ts new file mode 100644 index 000000000..f7b2a9706 --- /dev/null +++ b/src-ts/lib/generic-page-moved/index.ts @@ -0,0 +1 @@ +export { default as GenericPageMoved } from './GenericPageMoved' diff --git a/src-ts/lib/global-config.model.ts b/src-ts/lib/global-config.model.ts index 18820b175..6b848d853 100644 --- a/src-ts/lib/global-config.model.ts +++ b/src-ts/lib/global-config.model.ts @@ -29,12 +29,14 @@ export interface GlobalConfig { CUSTOMER_TOKEN: string } TOPCODER_URLS: { + ACCOUNT_PROFILE: string ACCOUNT_SETTINGS: string API_BASE: string BLOG_PAGE: string CHALLENGES_PAGE: string GIGS_PAGE: string THRIVE_PAGE: string + TCA: string USER_PROFILE: string WP_CONTENT: string } diff --git a/src-ts/lib/hooks/index.ts b/src-ts/lib/hooks/index.ts index 920935edc..2d616e17e 100644 --- a/src-ts/lib/hooks/index.ts +++ b/src-ts/lib/hooks/index.ts @@ -2,4 +2,6 @@ export * from './use-check-is-mobile.hook' export * from './use-click-outside.hook' export * from './use-on-hover-element.hook' export * from './use-storage.hook' +export * from './use-viewport-units-fix.hook' export * from './use-window-size.hook' +export * from './use-navigate-back.hook' diff --git a/src-ts/lib/hooks/use-navigate-back.hook.ts b/src-ts/lib/hooks/use-navigate-back.hook.ts new file mode 100644 index 000000000..f51536668 --- /dev/null +++ b/src-ts/lib/hooks/use-navigate-back.hook.ts @@ -0,0 +1,20 @@ +import { NavigateFunction, useNavigate } from 'react-router-dom' + +export type NavigateBackFunction = (fallbackUrl: string) => void +type useNavigateBackType = () => NavigateBackFunction + +export const useNavigateBack: useNavigateBackType = (): NavigateBackFunction => { + const navigate: NavigateFunction = useNavigate() + return (fallbackUrl: string) => { + const currentPageHref: string = window.location.href + + window.history.go(-1) + + setTimeout(() => { + // go back didn't work, navigate to desired fallback url + if (window.location.href === currentPageHref) { + navigate(fallbackUrl) + } + }, 30) + } +} diff --git a/src-ts/lib/hooks/use-storage.hook.ts b/src-ts/lib/hooks/use-storage.hook.ts index 25b643cf0..a1109952f 100644 --- a/src-ts/lib/hooks/use-storage.hook.ts +++ b/src-ts/lib/hooks/use-storage.hook.ts @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction, useCallback, useState } from 'react' +import { Dispatch, MutableRefObject, SetStateAction, useCallback, useRef, useState } from 'react' type StorageTypes = 'localStorage' | 'sessionStorage' @@ -8,6 +8,7 @@ export function useStorage( initialValue?: T, ): [T, Dispatch>] { const storage: Storage = window[storageType] + const wasKey: MutableRefObject = useRef(storageKey) const readStoredValue: () => T = useCallback(() => { try { @@ -23,7 +24,17 @@ export function useStorage( // State to store our value // Pass initial state function to useState so logic is only executed once - const [storedValue, setStoredValue]: [T, Dispatch>] = useState(readStoredValue()) + const rawStoredValue: T = readStoredValue() + const [storedValue, setStoredValue]: [T, Dispatch>] = useState(rawStoredValue) + + // update value when storage key changes + if (wasKey.current !== storageKey) { + wasKey.current = storageKey + + if (rawStoredValue !== storedValue) { + setTimeout(setStoredValue, 0, rawStoredValue) + } + } // Return a wrapped version of useState's setter function that // persists the new value to local or session storage. diff --git a/src-ts/lib/hooks/use-viewport-units-fix.hook.ts b/src-ts/lib/hooks/use-viewport-units-fix.hook.ts new file mode 100644 index 000000000..28fe6d629 --- /dev/null +++ b/src-ts/lib/hooks/use-viewport-units-fix.hook.ts @@ -0,0 +1,21 @@ +import { MutableRefObject, useRef } from 'react' + +import { useWindowSize, WindowSize } from './use-window-size.hook' + +/** + * On mobile, vh units are not consistent accross devices/browsers, + * this hook listen to window resize and fixes sets a --vh CSS variable + * on the document root, so we can access it everywhere + * + * @see https://css-tricks.com/the-trick-to-viewport-units-on-mobile + */ +export function useViewportUnitsFix(): void { + const { height }: WindowSize = useWindowSize() + const wasHeight: MutableRefObject = useRef(height) + + if (wasHeight.current !== height) { + // We execute the same script as before + const vh: number = window.innerHeight * 0.01 + document.documentElement.style.setProperty('--vh', `${vh}px`) + } +} diff --git a/src-ts/lib/index.ts b/src-ts/lib/index.ts index 99e14a759..ebd2e945e 100644 --- a/src-ts/lib/index.ts +++ b/src-ts/lib/index.ts @@ -34,6 +34,7 @@ export * from './generic-data-object.type' export * from './global-config.model' export * from './hooks' export * from './info-card' +export * from './generic-page-moved' export * from './loading-spinner' export * from './modals' export * from './page-divider' @@ -51,3 +52,4 @@ export * from './radio-button' export * from './payment-form' export * from './react-select' export * from './social-share' +export * from './default-member-icon' diff --git a/src-ts/lib/modals/base-modal/BaseModal.module.scss b/src-ts/lib/modals/base-modal/BaseModal.module.scss index c1632cf06..de24f6b51 100644 --- a/src-ts/lib/modals/base-modal/BaseModal.module.scss +++ b/src-ts/lib/modals/base-modal/BaseModal.module.scss @@ -17,6 +17,19 @@ margin: 24px 0; } +.buttonsWrap { + position: relative; + background: $tc-white; +} + +.buttonContainer { + display: flex; + justify-content: flex-end; + gap: 16px; + align-items: center; + flex: 0 0 auto; +} + .modal-body { flex: 1 1 auto; overflow: auto; @@ -28,12 +41,8 @@ @extend .body-main; & :global(.button-container) { - display: flex; - justify-content: flex-end; - gap: 16px; - align-items: center; + @extend .buttonContainer; margin-top: 24px; - flex: 0 0 auto; } @include ltemd { @@ -41,7 +50,7 @@ padding: 0 $space-lg $space-lg; } - :global(.button-container) { + :global(.button-container) { margin-top: $space-sm; } } diff --git a/src-ts/lib/modals/base-modal/BaseModal.tsx b/src-ts/lib/modals/base-modal/BaseModal.tsx index c20b2f67e..7e7e1d2ee 100644 --- a/src-ts/lib/modals/base-modal/BaseModal.tsx +++ b/src-ts/lib/modals/base-modal/BaseModal.tsx @@ -1,4 +1,4 @@ -import { FC, ReactNode } from 'react' +import { FC, ReactNode, useEffect } from 'react' import Modal, { ModalProps } from 'react-responsive-modal' import classNames from 'classnames' @@ -11,8 +11,9 @@ import styles from './BaseModal.module.scss' export interface BaseModalProps extends ModalProps { contentClassName?: string contentUrl?: string - size?: 'lg' | 'md' - title: string + size?: 'body' | 'lg' | 'md' | 'sm' + title?: string + buttons?: ReactNode } const BaseModal: FC = (props: BaseModalProps) => { @@ -36,22 +37,41 @@ const BaseModal: FC = (props: BaseModalProps) => { ) } + useEffect(() => { + if (props.blockScroll === false) { + document.documentElement.style.overflow = props.open ? 'hidden' : '' + document.body.style.overflow = props.open ? 'hidden' : '' + } + }, [props.blockScroll, props.open]) + return ( } > -
-

{props.title}

-
+ {props.title && ( + <> +
+

{props.title}

+
-
+
+ + )}
{renterContent()} {props.children}
+ {props.buttons && ( +
+
+
+ {props.buttons} +
+
+ )}
) } diff --git a/src-ts/lib/modals/base-modal/index.ts b/src-ts/lib/modals/base-modal/index.ts index 69e0ee8a1..877579c0e 100644 --- a/src-ts/lib/modals/base-modal/index.ts +++ b/src-ts/lib/modals/base-modal/index.ts @@ -1 +1 @@ -export { default as BaseModal } from './BaseModal' +export { default as BaseModal, type BaseModalProps } from './BaseModal' diff --git a/src-ts/lib/page-divider/PageDivider.tsx b/src-ts/lib/page-divider/PageDivider.tsx index 8af0479ca..1b577767c 100644 --- a/src-ts/lib/page-divider/PageDivider.tsx +++ b/src-ts/lib/page-divider/PageDivider.tsx @@ -11,7 +11,7 @@ const PageDivider: FC = ({ styleNames = [] }) => { const additionalStyles: Array<{ [key: string]: any }> = styleNames.map(style => styles[style]) - return
+ return
} export default PageDivider diff --git a/src-ts/lib/page-footer/PageFooter.tsx b/src-ts/lib/page-footer/PageFooter.tsx index 9b3bdfbfa..b8cac6728 100644 --- a/src-ts/lib/page-footer/PageFooter.tsx +++ b/src-ts/lib/page-footer/PageFooter.tsx @@ -19,7 +19,7 @@ const PageFooter: FC<{}> = () => { ) }, 10) - return
+ return <> } export default PageFooter diff --git a/src-ts/lib/progress-bar/ProgressBar.module.scss b/src-ts/lib/progress-bar/ProgressBar.module.scss index eabd01c2a..c2139c92d 100644 --- a/src-ts/lib/progress-bar/ProgressBar.module.scss +++ b/src-ts/lib/progress-bar/ProgressBar.module.scss @@ -3,14 +3,68 @@ .wrap { background: $black-10; border-radius: $space-xs; - height: $space-sm; + height: $space-lg; width: 100%; - display: flex; :global(.progress) { background: $turq-75; border-radius: inherit; width: calc(var(--progress, 0) * 100%); + position: relative; + + .percentage { + position: absolute; + top: 1px; + font-family: $font-barlow; + font-style: normal; + font-weight: $font-weight-bold; + font-size: 11px; + line-height: 14px; + color: $black-100; + } + } + + :global(.completed) { + border-radius: inherit; + width: calc(var(--progress, 0) * 100%); + position: relative; + + .completedText { + position: absolute; + top: 1px; + left: $space-xs; + font-family: $font-barlow; + font-style: normal; + font-weight: $font-weight-bold; + font-size: 11px; + line-height: 14px; + text-transform: uppercase; + color: $tc-white; + } + } + + :global(.completed-dev) { + background: $tc-dev-grad; + } + + :global(.completed-datascience) { + background: $tc-datascience-grad; + } + + :global(.completed-design) { + background: $tc-design-grad; + } + + :global(.completed-qa) { + background: $tc-qa-grad; + } + + :global(.completed-interview) { + background: $tc-interview-grad; + } + + :global(.completed-security) { + background: $tc-security-grad; } -} +} \ No newline at end of file diff --git a/src-ts/lib/progress-bar/ProgressBar.tsx b/src-ts/lib/progress-bar/ProgressBar.tsx index 694175062..047f5cb93 100644 --- a/src-ts/lib/progress-bar/ProgressBar.tsx +++ b/src-ts/lib/progress-bar/ProgressBar.tsx @@ -1,20 +1,50 @@ import React, { FC } from 'react' +import classNames from 'classnames' import styles from './ProgressBar.module.scss' interface ProgressBarProps { progress: number + track?: string } const ProgressBar: FC = (props: ProgressBarProps) => { - const progressProps: React.CSSProperties & {'--progress': number} = { + const progressProps: React.CSSProperties & { '--progress': number } = { '--progress': props.progress, } + const persentageProps: React.CSSProperties = { + [props.progress >= 0.05 ? 'right' : 'left']: '4px', + } + + const showPercentage: boolean = props.progress > 0 && props.progress < 1 + const showCompleted: boolean = props.progress === 1 + return (
-
+
+ { + !!showPercentage && ( + + {Number(props.progress * 100) + .toFixed(0)} + % + + ) + } + { + !!showCompleted && ( + Completed + ) + } +
) } diff --git a/src-ts/lib/styles/_layout.scss b/src-ts/lib/styles/_layout.scss index 55dcfcc67..b92b8168e 100644 --- a/src-ts/lib/styles/_layout.scss +++ b/src-ts/lib/styles/_layout.scss @@ -2,18 +2,16 @@ @import 'mixins'; html { - --header-height: 80px; - --footer-height: 51px; + --header-height: 60px; + --footer-height: 96px; --breadcrumb-height: 64px; - @include ltemd { - --header-height: 64px; - --breadcrumb-height: 48px; + @media (max-width: 767px) { + --header-height: 48px; } - @include ltesm { - --header-height: 48px; - --footer-height: 101px; + @include ltemd { + --breadcrumb-height: 48px; } } diff --git a/src-ts/lib/styles/_modals.scss b/src-ts/lib/styles/_modals.scss index aac139069..ee2d8b361 100644 --- a/src-ts/lib/styles/_modals.scss +++ b/src-ts/lib/styles/_modals.scss @@ -10,11 +10,19 @@ display: flex; flex-direction: column; width: 100vw; - height: 100vh; + height: calc(var(--vh, 1vh) * 100); margin: auto; border-radius: 0; padding: $space-xxl $space-xxxxl $space-xxxxl; + &.modal-sm { + width: 361px; + min-width: auto; + @include ltemd { + width: 100vw; + } + } + &.modal-md { width: 450px; } @@ -23,6 +31,14 @@ width: 700px; } + &.modal-body { + width: 90vw; + max-width: 1000px; + @include ltemd { + width: 100vw; + } + } + @include ltemd { max-width: 450px; padding: $space-xxl $space-lg; @@ -55,4 +71,4 @@ } } } -} \ No newline at end of file +} diff --git a/src-ts/lib/styles/_typography.scss b/src-ts/lib/styles/_typography.scss index 8def26805..9585bd470 100644 --- a/src-ts/lib/styles/_typography.scss +++ b/src-ts/lib/styles/_typography.scss @@ -53,11 +53,16 @@ h2 { h3 { font-size: 22px; line-height: 26px; - + @include ltemd { font-size: 18px; line-height: 20px; } + + &.marketing { + font-size: 44px; + line-height: 44px; + } } h4 { @@ -134,13 +139,17 @@ h4 { font-family: $font-roboto; font-size: 16px; line-height: 24px; + &-medium { + @extend .body-main; + font-weight: $font-weight-medium; + } &-bold { @extend .body-main; font-weight: $font-weight-bold; } @include ltemd { - &, &-bold { + &, &-medium, &-bold { font-size: 14px; line-height: 20px; } @@ -177,7 +186,7 @@ h4 { font-weight: $font-weight-semibold; font-size: 20px; line-height: 26px; - + @include ltemd { font-size: 14px; line-height: 20px; @@ -188,7 +197,7 @@ h4 { font-weight: $font-weight-normal; font-size: 20px; line-height: 26px; - + @include ltemd { font-size: 14px; line-height: 20px; @@ -201,7 +210,7 @@ h4 { padding: 0; font-size: 20px; line-height: 22px; - + @include ltemd { font-size: 14px; line-height: 20px; @@ -231,13 +240,15 @@ h4 { } .body-ultra-small, -.body-ultra-small-medium { +.body-ultra-small-medium, +.body-ultra-small-bold { font-size: 12px; line-height: 18px; font-weight: $font-weight-normal; } -.body-ultra-small-medium { +.body-ultra-small-medium, +.body-ultra-small-bold { font-weight: $font-weight-bold; } @@ -278,6 +289,14 @@ h4 { font-style: revert; } +.quote-main { + font-size: 16px; + line-height: 26px; + font-family: $font-roboto; + font-weight: $font-weight-normal; + font-style: italic; +} + .label { font-weight: $font-weight-semibold; font-size: 11px; diff --git a/src-ts/lib/styles/mixins/_breakpoints.mixins.scss b/src-ts/lib/styles/mixins/_breakpoints.mixins.scss index 878ab1b20..9f6cb1893 100644 --- a/src-ts/lib/styles/mixins/_breakpoints.mixins.scss +++ b/src-ts/lib/styles/mixins/_breakpoints.mixins.scss @@ -85,12 +85,30 @@ } } +@mixin ltexl { + @media (max-width: #{$xl-max}) { + @content; + } +} + @mixin xl { @media (min-width: #{$xl-min}) and (max-width: #{$xl-max}){ @content; } } +@mixin gtexl { + @media (min-width: #{$xl-min}) { + @content; + } +} + +@mixin ltexxl { + @media (max-width: #{$xl-max}) { + @content; + } +} + @mixin xxl { @media (min-width: #{$xxl-min}){ @content; diff --git a/src-ts/lib/styles/mixins/_layout.mixins.scss b/src-ts/lib/styles/mixins/_layout.mixins.scss index 4e5af370a..ffecff189 100644 --- a/src-ts/lib/styles/mixins/_layout.mixins.scss +++ b/src-ts/lib/styles/mixins/_layout.mixins.scss @@ -9,12 +9,27 @@ padding-right: $space-xxl; } - @include xxs { + @include ltesm { padding-left: $space-lg; padding-right: $space-lg; } } +@mixin substractPagePaddings { + margin-left: -$space-xxxxl; + margin-right: -$space-xxxxl; + + @include ltemd { + margin-left: -$space-xxl; + margin-right: -$space-xxl; + } + + @include ltesm { + margin-left: -$space-lg; + margin-right: -$space-lg; + } +} + @mixin contentWidth { max-width: $xxl-min; @include pagePaddings; @@ -28,6 +43,7 @@ } @mixin scrollbar { + // firefox's solution for "customizing" scrollbars & { scrollbar-width: thin; @@ -52,3 +68,20 @@ } } } + +@mixin freeLabel { + font-size: 24px; + line-height: 32px; + font-weight: $font-weight-bold; + font-style: normal; + background-color: $orange-100; + padding: 2px $space-sm; + border-radius: 4px; + color: $black-100; + display: inline-block; + + @include ltesm { + font-size: 20px; + line-height: 28px; + } +} \ No newline at end of file diff --git a/src-ts/lib/styles/variables/_constants.scss b/src-ts/lib/styles/variables/_constants.scss new file mode 100644 index 000000000..3ef3f8739 --- /dev/null +++ b/src-ts/lib/styles/variables/_constants.scss @@ -0,0 +1 @@ +$tca-certif-aspect-ratio: 1.2571; diff --git a/src-ts/lib/styles/variables/_palette.scss b/src-ts/lib/styles/variables/_palette.scss index 1c163c58d..22aa06c5e 100644 --- a/src-ts/lib/styles/variables/_palette.scss +++ b/src-ts/lib/styles/variables/_palette.scss @@ -198,6 +198,7 @@ $tc-grad16: linear-gradient(265.38deg, $turq-100 1.99%, $teal-100 98.19%); $tc-grad17: linear-gradient(265.38deg, #363D8C 1.99%, #723390 98.19%); $tc-grad18: linear-gradient(84.92deg, #363D8C 2.08%, #723390 97.43%); $tc-grad19: linear-gradient(83.58deg, #7B21A7 2.28%, #1974AD 97.67%); +$tc-grad20: linear-gradient(30deg, #05456D 2.12%, #0A7AC0 97.43%); /* OPACITY */ @@ -206,3 +207,22 @@ $white-100-opacity-10: rgba(0, 0, 0, 0.1); /* SHADOW */ $tips-shadow: 0 1px 6px 1px rgba(0, 0, 0, 0.2); + +/* TRACK COLORS & GRADIENTS */ +$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; +} \ No newline at end of file diff --git a/src-ts/lib/styles/variables/index.scss b/src-ts/lib/styles/variables/index.scss index d6491d396..39fe84bc3 100644 --- a/src-ts/lib/styles/variables/index.scss +++ b/src-ts/lib/styles/variables/index.scss @@ -1,3 +1,4 @@ +@import './constants'; @import './breakpoints'; @import './fonts'; @import './layouts'; diff --git a/src-ts/lib/svgs/icon-user-default.svg b/src-ts/lib/svgs/icon-user-default.svg new file mode 100644 index 000000000..052ea460f --- /dev/null +++ b/src-ts/lib/svgs/icon-user-default.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src-ts/lib/svgs/index.ts b/src-ts/lib/svgs/index.ts index 3d20722c1..7d4738adb 100644 --- a/src-ts/lib/svgs/index.ts +++ b/src-ts/lib/svgs/index.ts @@ -19,6 +19,14 @@ import { ReactComponent as SocialShareTwitter } from './social-share-twitter.svg import { ReactComponent as SocialIconTwitter } from './social-tw-icon.svg' import { ReactComponent as SocialIconYoutube } from './social-yt-icon.svg' import { ReactComponent as TooltipArrowIcon } from './tooltip-arrow.svg' +import { ReactComponent as TcAcademyLogoSvg } from './tc-academy-logo.svg' +import { ReactComponent as TCAcademyLogoWhiteSvg } from './tc-academy-logo-white.svg' +import { ReactComponent as TCAcademyLogoMixedSvg } from './tc-academy-logo-mixed.svg' +import { ReactComponent as TcLogoSvg } from './tc-logo.svg' +import { ReactComponent as TCLogoSvg } from './tc-logo-white.svg' +import { ReactComponent as FccLogoSvg } from './vendor-fcc-logo.svg' +import { ReactComponent as FccLogoBlackSvg } from './vendor-fcc-logo-black.svg' +import { ReactComponent as VerifiedMemberFlagSvg } from './verified-member-flag.svg' export { ActiveTabTipIcon, @@ -40,5 +48,13 @@ export { GithubIcon, SaveForLaterIcon, IconCheck, + TcAcademyLogoSvg, + TCAcademyLogoWhiteSvg, + TCAcademyLogoMixedSvg, + TcLogoSvg, + TCLogoSvg, + FccLogoSvg, + FccLogoBlackSvg, + VerifiedMemberFlagSvg, } export * from './icon-wrapper' diff --git a/src-ts/lib/svgs/tc-academy-logo-mixed.svg b/src-ts/lib/svgs/tc-academy-logo-mixed.svg new file mode 100644 index 000000000..261b103e7 --- /dev/null +++ b/src-ts/lib/svgs/tc-academy-logo-mixed.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/lib/svgs/tc-academy-logo-white.svg b/src-ts/lib/svgs/tc-academy-logo-white.svg new file mode 100644 index 000000000..605aef6a4 --- /dev/null +++ b/src-ts/lib/svgs/tc-academy-logo-white.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/course-certificate/certificate-view/certificate/tc-academy-logo.svg b/src-ts/lib/svgs/tc-academy-logo.svg similarity index 100% rename from src-ts/tools/learn/course-certificate/certificate-view/certificate/tc-academy-logo.svg rename to src-ts/lib/svgs/tc-academy-logo.svg diff --git a/src-ts/lib/svgs/tc-logo-white.svg b/src-ts/lib/svgs/tc-logo-white.svg new file mode 100644 index 000000000..6a3996f43 --- /dev/null +++ b/src-ts/lib/svgs/tc-logo-white.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src-ts/tools/learn/course-certificate/certificate-view/certificate/tc-logo.svg b/src-ts/lib/svgs/tc-logo.svg similarity index 100% rename from src-ts/tools/learn/course-certificate/certificate-view/certificate/tc-logo.svg rename to src-ts/lib/svgs/tc-logo.svg diff --git a/src-ts/lib/svgs/vendor-fcc-logo-black.svg b/src-ts/lib/svgs/vendor-fcc-logo-black.svg new file mode 100644 index 000000000..8cda25da7 --- /dev/null +++ b/src-ts/lib/svgs/vendor-fcc-logo-black.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/course-certificate/certificate-view/certificate/vendor-fcc-logo.svg b/src-ts/lib/svgs/vendor-fcc-logo.svg similarity index 100% rename from src-ts/tools/learn/course-certificate/certificate-view/certificate/vendor-fcc-logo.svg rename to src-ts/lib/svgs/vendor-fcc-logo.svg diff --git a/src-ts/lib/svgs/verified-member-flag.svg b/src-ts/lib/svgs/verified-member-flag.svg new file mode 100644 index 000000000..e95bc1c60 --- /dev/null +++ b/src-ts/lib/svgs/verified-member-flag.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src-ts/lib/tooltip/Tooltip.tsx b/src-ts/lib/tooltip/Tooltip.tsx index 3e8c84c96..bcbcf230c 100644 --- a/src-ts/lib/tooltip/Tooltip.tsx +++ b/src-ts/lib/tooltip/Tooltip.tsx @@ -9,7 +9,7 @@ interface TooltipEvent { } interface TooltipProps { - content?: string + content?: ReactNode place?: 'top' | 'right' | 'bottom' | 'left' trigger: ReactNode triggerOn?: 'click' | 'hover' diff --git a/src-ts/tools/learn/certification-details/CertificationDetailsPage.module.scss b/src-ts/tools/learn/certification-details/CertificationDetailsPage.module.scss new file mode 100644 index 000000000..6f4443f79 --- /dev/null +++ b/src-ts/tools/learn/certification-details/CertificationDetailsPage.module.scss @@ -0,0 +1,23 @@ +@import '../../../lib/styles/includes'; + +.text-section.text-section { + margin-top: 80px; + + @include ltelg { + margin-top: $space-xxxxl; + } + + > h2 { + margin-bottom: $space-xxl; + } + + > ul { + list-style: disc; + padding-left: $space-xxl; + padding-left: $space-xxl; + } + + &.no-top { + margin-top: 0; + } +} diff --git a/src-ts/tools/learn/certification-details/CertificationDetailsPage.tsx b/src-ts/tools/learn/certification-details/CertificationDetailsPage.tsx new file mode 100644 index 000000000..b0d1e150a --- /dev/null +++ b/src-ts/tools/learn/certification-details/CertificationDetailsPage.tsx @@ -0,0 +1,130 @@ +import { Dispatch, FC, ReactNode, SetStateAction, useContext, useState } from 'react' +import { Params, useParams } from 'react-router-dom' +import classNames from 'classnames' + +import { + TCACertificationProgressProviderData, + TCACertificationProgressStatus, + TCACertificationProviderData, + useGetTCACertification, + useGetTCACertificationProgress, + useGetUserCertifications, + UserCertificationsProviderData, +} from '../learn-lib' +import { + Button, + profileContext, + ProfileContextData, +} from '../../../lib' + +import { CertificationDetailsSidebar } from './certification-details-sidebar' +import { CertificationCurriculum } from './certification-curriculum' +import { EnrollCtaBtn } from './enroll-cta-btn' +import { CertifDetailsContent, CertificationDetailsModal } from './certification-details-modal' +import { PageLayout } from './page-layout' +import styles from './CertificationDetailsPage.module.scss' + +const CertificationDetailsPage: FC<{}> = () => { + const routeParams: Params = useParams() + const { certification: dashedName }: Params = routeParams + const { initialized: profileReady, profile }: ProfileContextData = useContext(profileContext) + + const [isCertifDetailsModalOpen, setCertifDetailsModalOpen]: [boolean, Dispatch>] + = useState(false) + + // Fetch the User's progress for all the courses + // so we can show their progress + // even before they enroll with the certification + const { + progresses: certsProgress, + ready: certsProgressReady, + }: UserCertificationsProviderData = useGetUserCertifications() + + const { + certification, + ready: certificationReady, + }: TCACertificationProviderData = useGetTCACertification(dashedName as string) + + // Fetch Enrollment status & progress + const { + progress, + ready: progressReady, + }: TCACertificationProgressProviderData = useGetTCACertificationProgress( + profile?.userId as unknown as string, + dashedName as string, + { enabled: profileReady && !!profile }, + ) + + const ready: boolean = profileReady && certificationReady && (!profile || (progressReady && certsProgressReady)) + + const isEnrolled: boolean = progressReady && !!progress + const isNotEnrolledView: boolean = !progressReady || !progress + const isCompleted: boolean = progress?.status === TCACertificationProgressStatus.completed + + function renderCertificationCurriculum(): ReactNode { + return ( +
+ +
+ ) + } + + function toggleCertifDetailsModal(): void { + setCertifDetailsModalOpen(d => !d) + } + + function renderMainContent(): ReactNode { + return ready ? ( + isNotEnrolledView ? ( + + {renderCertificationCurriculum()} + + ) : ( + <> + {renderCertificationCurriculum()} +
+
+ + + ) + ) : null + } + + function renderSidebar(): ReactNode { + return ( + + ) + } + + return ( + + )} + /> + ) +} + +export default CertificationDetailsPage diff --git a/src-ts/tools/learn/certification-details/accordion/Accordion.module.scss b/src-ts/tools/learn/certification-details/accordion/Accordion.module.scss new file mode 100644 index 000000000..627e7784b --- /dev/null +++ b/src-ts/tools/learn/certification-details/accordion/Accordion.module.scss @@ -0,0 +1,5 @@ +@import '../../../../lib/styles/includes'; + +.wrap { + +} diff --git a/src-ts/tools/learn/certification-details/accordion/Accordion.tsx b/src-ts/tools/learn/certification-details/accordion/Accordion.tsx new file mode 100644 index 000000000..798975798 --- /dev/null +++ b/src-ts/tools/learn/certification-details/accordion/Accordion.tsx @@ -0,0 +1,41 @@ +import { Dispatch, FC, SetStateAction, useState } from 'react' + +import { AccordionItem } from './accordion-item' +import styles from './Accordion.module.scss' + +export interface FaqEntry { + title: string + description: string +} + +interface AccordionProps { + items: Array +} + +type ToggledItems = { + [key: string]: boolean +} + +const Accordion: FC = (props: AccordionProps) => { + + const [toggled, setToggled]: [ToggledItems, Dispatch>] = useState({} as ToggledItems) + + function toggle(item: FaqEntry): void { + setToggled(t => ({ ...t, [item.title]: !t[item.title] })) + } + + return ( +
+ {props.items.map((item: FaqEntry) => ( + + ))} +
+ ) +} + +export default Accordion diff --git a/src-ts/tools/learn/certification-details/accordion/accordion-item/AccordionItem.module.scss b/src-ts/tools/learn/certification-details/accordion/accordion-item/AccordionItem.module.scss new file mode 100644 index 000000000..521067c00 --- /dev/null +++ b/src-ts/tools/learn/certification-details/accordion/accordion-item/AccordionItem.module.scss @@ -0,0 +1,51 @@ +@import '../../../../../lib/styles/includes'; + +.item { + padding: $space-lg $space-xxl; + border-top: $border solid $black-10; + &:last-child { + border-bottom: $border solid $black-10; + } + + &:global(.toggled) { + .itemArrowIcon { + transform: rotateZ(0deg); + } + } + &:global(:not(.toggled)) { + .itemDesc { + display: none; + } + } +} + +.itemTitle { + display: flex; + align-items: flex-start; + + cursor: pointer; +} + +.itemDesc { + margin-top: $space-lg; +} + +.itemArrowIcon { + display: flex; + width: 24px; + height: 24px; + align-items: center; + justify-content: center; + transform: rotateZ(180deg); + color: $turq-160; + + margin-left: auto; + + transition: 0.15s ease-in; + flex: 0 0 auto; + + svg { + display: block; + width: 100%; + } +} diff --git a/src-ts/tools/learn/certification-details/accordion/accordion-item/AccordionItem.tsx b/src-ts/tools/learn/certification-details/accordion/accordion-item/AccordionItem.tsx new file mode 100644 index 000000000..b02b7ec73 --- /dev/null +++ b/src-ts/tools/learn/certification-details/accordion/accordion-item/AccordionItem.tsx @@ -0,0 +1,47 @@ +import { FC } from 'react' +import classNames from 'classnames' + +import { type FaqEntry } from '../Accordion' +import { IconSolid } from '../../../../../lib' + +import styles from './AccordionItem.module.scss' + +interface AccordionItemProps { + item: FaqEntry + toggle: (item: FaqEntry) => void + isToggled: boolean +} + +const AccordionItem: FC = (props: AccordionItemProps) => { + function toggle(): void { + props.toggle(props.item) + } + + return ( +
+
+ {props.item.title} + + + +
+
+
+ ) +} + +export default AccordionItem diff --git a/src-ts/tools/learn/certification-details/accordion/accordion-item/index.ts b/src-ts/tools/learn/certification-details/accordion/accordion-item/index.ts new file mode 100644 index 000000000..895649bdc --- /dev/null +++ b/src-ts/tools/learn/certification-details/accordion/accordion-item/index.ts @@ -0,0 +1 @@ +export { default as AccordionItem } from './AccordionItem' diff --git a/src-ts/tools/learn/certification-details/accordion/index.ts b/src-ts/tools/learn/certification-details/accordion/index.ts new file mode 100644 index 000000000..971fd8afc --- /dev/null +++ b/src-ts/tools/learn/certification-details/accordion/index.ts @@ -0,0 +1 @@ +export { default as Accordion } from './Accordion' diff --git a/src-ts/tools/learn/certification-details/certification-curriculum/CertificationCurriculum.module.scss b/src-ts/tools/learn/certification-details/certification-curriculum/CertificationCurriculum.module.scss new file mode 100644 index 000000000..db6cc4c39 --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-curriculum/CertificationCurriculum.module.scss @@ -0,0 +1,113 @@ +@import '../../../../lib/styles/includes'; + +.wrap { + padding: $space-xxxxl; + background: linear-gradient(84.45deg, $blue-160 2.12%, $blue-110 97.43%); + border-radius: 8px; + color: $tc-white; + + + @include ltemd { + border-radius: 0; + margin-left: -$space-xxl; + margin-right: -$space-xxl; + } + + @include ltesm { + margin-left: -$space-lg; + margin-right: -$space-lg; + } + + @media (max-width: 1150px) { + padding: $space-xxxxl $space-lg $space-lg; + } +} + +.headline { + display: flex; + align-items: baseline; + + @include ltelg { + flex-direction: column; + } + + .headlineDetails { + margin-left: auto; + + display: flex; + align-items: baseline; + gap: $space-lg; + + @media (max-width: 1250px) { + flex-wrap: wrap; + justify-content: flex-end; + } + + @include ltelg { + margin-left: 0; + margin-top: $space-xxl; + } + } +} + +.headlineDetailsItem { + display: flex; + align-items: baseline; + gap: $space-xs; + + .icon { + height: 38px; + padding: 8px 0 0px; + } + + svg { + display: block; + @include icon-xxxxl; + color: $turq-75; + } + + > span, .headlineDetailsSpan { + @include font-barlow; + font-weight: 600; + font-size: 18px; + line-height: 22px; + + color: $black-20; + text-transform: uppercase; + } + + strong { + @include font-barlow-condensed; + font-weight: 500; + font-size: 44px; + line-height: 44px; + + text-transform: uppercase; + + :global(span.units) { + @extend .headlineDetailsSpan; + align-self: flex-end; + padding-bottom: 1px; + } + } +} + +.container { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0) 100%); + border: 2px dashed rgba(255, 255, 255, 0.6); + border-radius: 8px; + padding: $space-xxxxl; + + margin-top: $space-xxl; + @media (max-width: 1150px) { + padding: $space-lg; + } +} + +.courses { + display: flex; + flex-direction: column; + gap: $space-lg; + + margin-bottom: $space-mx; +} diff --git a/src-ts/tools/learn/certification-details/certification-curriculum/CertificationCurriculum.tsx b/src-ts/tools/learn/certification-details/certification-curriculum/CertificationCurriculum.tsx new file mode 100644 index 000000000..815307293 --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-curriculum/CertificationCurriculum.tsx @@ -0,0 +1,97 @@ +import { FC, useMemo } from 'react' +import { get } from 'lodash' + +import { IconOutline } from '../../../../lib' +import { + CompletionTimeRange, + LearnUserCertificationProgress, + TCACertification, + TCACertificationProvider, +} from '../../learn-lib' + +import { CertificationSummary } from './certification-summary' +import { AssessmentCard, CourseCard } from './curriculum-cards' +import styles from './CertificationCurriculum.module.scss' + +interface CertificationCurriculumProps { + certification: TCACertification + certsProgress?: ReadonlyArray + isEnrolled: boolean + isCompleted: boolean +} + +interface ProgressByIdCollection { + [key: string]: LearnUserCertificationProgress +} + +interface ProvidersByIdCollection { + [key: string]: TCACertificationProvider +} + +const CertificationCurriculum: FC = (props: CertificationCurriculumProps) => { + const progressById: ProgressByIdCollection = useMemo(() => ( + props.certsProgress?.reduce((all, progress) => { + all[progress.certificationId] = progress + return all + }, {} as ProgressByIdCollection) ?? {} + ), [props.certsProgress]) + + const providersById: ProvidersByIdCollection = useMemo(() => ( + props.certification.resourceProviders.reduce((all, provider) => { + all[provider.id] = provider + return all + }, {} as ProvidersByIdCollection) + ), [props.certification]) + + return ( +
+
+

+ Certification Curriculum +

+
+
+
+ +
+ {props.certification.coursesCount} + courses +
+
+
+ +
+ + + +
+
+
+ +
+
+ {props.certification.certificationResources.map(cert => ( + + ))} + +
+ +
+
+ ) +} + +export default CertificationCurriculum diff --git a/src-ts/tools/learn/certification-details/certification-curriculum/certification-summary/CertificationSummary.module.scss b/src-ts/tools/learn/certification-details/certification-curriculum/certification-summary/CertificationSummary.module.scss new file mode 100644 index 000000000..666891ed3 --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-curriculum/certification-summary/CertificationSummary.module.scss @@ -0,0 +1,28 @@ +@import '../../../../../lib/styles/includes'; + +.wrap { + display: flex; + align-items: center; + gap: $space-lg; + + + > svg { + display: block; + @include icon-size(96); + } + + :global(.body-large-bold) { + > span { + margin-top: $space-sm; + display: block; + } + } +} + +.completedIcon { + margin-left: auto; + svg { + @include icon-size(52); + color: $turq-75; + } +} \ No newline at end of file diff --git a/src-ts/tools/learn/certification-details/certification-curriculum/certification-summary/CertificationSummary.tsx b/src-ts/tools/learn/certification-details/certification-curriculum/certification-summary/CertificationSummary.tsx new file mode 100644 index 000000000..59490dee7 --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-curriculum/certification-summary/CertificationSummary.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react' + +import { IconSolid } from '../../../../../lib' +import { CertificateBadgeIcon, TCACertification } from '../../../learn-lib' + +import styles from './CertificationSummary.module.scss' + +interface CertificationSummaryProps { + certification: TCACertification + isCompleted?: boolean +} + +const CertificationSummary: FC = (props: CertificationSummaryProps) => ( +
+ +
+

Certification

+ + {props.certification.title} + +
+ {props.isCompleted && ( +
+ +
+ )} +
+) + +export default CertificationSummary diff --git a/src-ts/tools/learn/certification-details/certification-curriculum/certification-summary/index.ts b/src-ts/tools/learn/certification-details/certification-curriculum/certification-summary/index.ts new file mode 100644 index 000000000..234b098d6 --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-curriculum/certification-summary/index.ts @@ -0,0 +1 @@ +export { default as CertificationSummary } from './CertificationSummary' 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 new file mode 100644 index 000000000..90a0e6060 --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/CurriculumCard.module.scss @@ -0,0 +1,144 @@ +@import '../../../../../lib/styles/includes'; + +.wrap { + border-radius: $space-sm; + background: $black-5; + padding: $space-xxl; + + display: flex; + gap: $space-xxl; + + @media (max-width: 1150px) { + padding: $space-lg; + flex-direction: column; + gap: $space-lg; + } +} + +.statusCol { + margin: -$space-xxl; + margin-right: 0; + + border-radius: $space-sm 0px 0px $space-sm; + overflow: hidden; + + width: 64px; + flex: 0 0 auto; + display: flex; + + @media (max-width: 1150px) { + display: none; + } +} + +.statusBox { + margin-left: auto; + @include icon-xxxxl; + border-radius: $space-xs; + display: flex; + overflow: hidden; +} + +.statusIcon { + display: flex; + align-items: center; + justify-content: center; + + background: $tc-white; + color: $black-40; + flex: 1 1 auto; + + &.inProgress { + color: $turq-100; + } + + &.completed { + background: $turq-100; + color: $tc-white; + } + + > svg { + display: flex; + @include icon-xxxxl; + + @include ltelg { + @include icon-xxl; + } + } +} + +.inner { + flex: 1 1 auto; + + display: flex; + align-items: flex-start; + gap: $space-lg; + + @include ltelg { + gap: $space-sm; + } +} + +.badge { + width: $space-mxx; + height: $space-mxx; + flex: 0 0 auto; + + @include ltelg { + width: $space-mx; + height: $space-mx; + } + + svg { + display: block; + width: 100%; + height: 100%; + } +} + +.contentWrap { + color: $black-100; + flex: 1 1 auto; +} + +.headline { + display: flex; + align-items: center; + gap: $space-sm; + + .cta { + margin-left: auto; + justify-content: flex-end; + + @media (max-width: 1250px) { + flex-wrap: wrap; + } + + @media (max-width: 1150px) { + display: none; + } + } +} + +.cta, .bottomActions, .bottomCta { + display: flex; + align-items: center; + gap: $space-sm; +} + +.bottomActions { + @media (min-width: 1150px) { + display: none; + } +} + +.bottomCta { + flex-direction: row-reverse; +} + +.content { + margin-top: $space-xs; + @include ltelg { + margin-top: 0; + } +} diff --git a/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/CurriculumCard.tsx b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/CurriculumCard.tsx new file mode 100644 index 000000000..409897c89 --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/CurriculumCard.tsx @@ -0,0 +1,84 @@ +import { FC, ReactNode } from 'react' +import classNames from 'classnames' + +import { CourseBadge, TCACertificateType, UserCertificationProgressStatus } from '../../../learn-lib' +import { IconOutline } from '../../../../../lib' + +import { ReactComponent as IconProgressSvg } from './progress-icon.svg' +import styles from './CurriculumCard.module.scss' + +interface CurriculumCardProps { + badgeTrackType: TCACertificateType + className?: string + content: ReactNode + cta: ReactNode + status?: UserCertificationProgressStatus + title: ReactNode +} + +const CurriculumCard: FC = (props: CurriculumCardProps) => { + function renderStatusCol(): ReactNode { + switch (props.status) { + case UserCertificationProgressStatus.completed: + return ( +
+ +
+ ) + case UserCertificationProgressStatus.inProgress: + return ( +
+ +
+ ) + default: + return ( +
+ +
+ ) + } + } + + return ( +
+ {props.status && ( +
+ {renderStatusCol()} +
+ )} +
+ + +
+
+
+ {props.title ?? 'Responsive Web Design Certification'} +
+
+ {props.cta} +
+
+
+ {props.content} +
+
+
+
+
+ {props.cta} +
+ {props.status && ( +
+ {renderStatusCol()} +
+ )} +
+
+ ) +} + +export default CurriculumCard diff --git a/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/assessment-card/AssessmentCard.module.scss b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/assessment-card/AssessmentCard.module.scss new file mode 100644 index 000000000..1ab2a0e19 --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/assessment-card/AssessmentCard.module.scss @@ -0,0 +1,39 @@ +@import '../../../../../../lib/styles/includes'; + +.card { + :global(.badge) { + opacity: 0.5; + } +} + +.cta { + color: $blue-140; +} + +.content { + display: flex; + align-items: center; + gap: $space-sm; + + @include ltelg { + flex-direction: column; + align-items: flex-start; + gap: $space-xs; + margin-top: $space-sm; + } + + :global(.quote-small) { + font-style: italic; + color: $black-60; + } +} + +.tag { + padding: 2px $space-xs 0; + background: $blue-140; + border-radius: $border; + + color: $tc-white; + text-transform: uppercase; + display: flex; +} diff --git a/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/assessment-card/AssessmentCard.tsx b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/assessment-card/AssessmentCard.tsx new file mode 100644 index 000000000..99ea75a3b --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/assessment-card/AssessmentCard.tsx @@ -0,0 +1,37 @@ +import { FC } from 'react' +import classNames from 'classnames' + +import { TCACertificateType } from '../../../../learn-lib' +import CurriculumCard from '../CurriculumCard' + +import styles from './AssessmentCard.module.scss' + +interface AssessmentCardProps { + trackType: TCACertificateType + title: string +} + +const AssessmentCard: FC = (props: AssessmentCardProps) => ( + Coming Soon + )} + content={( + <> +
+
+ Assessment +
+
+ Exclusive access to Assessments to further demonstrate your skills +
+
+ + )} + /> +) + +export default AssessmentCard diff --git a/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/assessment-card/index.ts b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/assessment-card/index.ts new file mode 100644 index 000000000..eede85640 --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/assessment-card/index.ts @@ -0,0 +1 @@ +export { default as AssessmentCard } from './AssessmentCard' diff --git a/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/course-card/CourseCard.module.scss b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/course-card/CourseCard.module.scss new file mode 100644 index 000000000..e6bf56b93 --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/course-card/CourseCard.module.scss @@ -0,0 +1,51 @@ +@import '../../../../../../lib/styles/includes'; + +.stats { + display: flex; + align-items: center; + padding: 0px; + gap: $space-lg; + + color: $black-60; + + margin-top: $space-xs; + @media (max-width: 1150px) { + flex-wrap: wrap; + } + + @include ltelg { + gap: 0 5px; + margin-top: 0; + } + + .stat { + display: flex; + align-items: center; + gap: $space-xs; + @include ltelg { + gap: 3px; + } + + .icon { + width: 16px; + height: 16px; + color: $blue-140; + > svg { + display: block; + width: 100%; + height: 100%; + } + } + } +} + +.providers { + margin-top: $space-xs; + @include ltelg { + margin-top: 0; + } +} + +.progress { + margin-top: $space-md; +} 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 new file mode 100644 index 000000000..56f61b82f --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-curriculum/curriculum-cards/course-card/CourseCard.tsx @@ -0,0 +1,143 @@ +import { FC, ReactNode } from 'react' + +import { Button, IconSolid, ProgressBar } from '../../../../../../lib' +import { + clearFCCCertificationTitle, + CompletionTimeRange, + LearnCertification, + LearnLevelIcon, + LearnUserCertificationProgress, + ProvidersLogoList, + TCACertificationCompletionTimeRange, + TCACertificationLearnLevel, + TCACertificationProviderBase, + useHoursEstimateToRange, + UserCertificationProgressStatus, +} from '../../../../learn-lib' +import { + getCertificatePath, + getCoursePath, + getLessonPathFromCurrentLesson, +} from '../../../../learn.routes' +import CurriculumCard from '../CurriculumCard' + +import styles from './CourseCard.module.scss' + +interface CourseCardProps { + certification: LearnCertification + progress: LearnUserCertificationProgress + learnerLevel: TCACertificationLearnLevel + provider: string + isEnrolled: boolean +} + +const CourseCard: FC = (props: CourseCardProps) => { + function renderCta(): ReactNode { + switch (props.progress?.status) { + case UserCertificationProgressStatus.completed: + return ( + <> +
+ )} +
    +
  • + + + + {props.certification.learnerLevel} +
  • +
  • + + + + + {props.certification.coursesCount} + {' courses'} + +
  • +
  • + + + + + + , [ + 'Assuming 1 to 4 hour', + 'learning per day', + ])} + place='bottom' + trigger={} + triggerOn='hover' + /> + +
  • + {!props.certProgress && ( +
  • + + + + + FREE +  until April 30  + $20 + +
  • + )} +
+ +
+ Skills Covered +
+
    + {props.certification.skills.map(skill => ( +
  • {skill}
  • + ))} +
+ + + +
+ {(props.enrolled || completed) ? ( +
+ {completed ? 'Completed' : 'Enrolled'} +
+ ) : ( + + )} +
+ + ) +} + +export default CertificationDetailsSidebar diff --git a/src-ts/tools/learn/certification-details/certification-details-sidebar/index.ts b/src-ts/tools/learn/certification-details/certification-details-sidebar/index.ts new file mode 100644 index 000000000..569c39964 --- /dev/null +++ b/src-ts/tools/learn/certification-details/certification-details-sidebar/index.ts @@ -0,0 +1 @@ +export { default as CertificationDetailsSidebar } from './CertificationDetailsSidebar' diff --git a/src-ts/tools/learn/certification-details/enroll-cta-btn/EnrollCtaBtn.tsx b/src-ts/tools/learn/certification-details/enroll-cta-btn/EnrollCtaBtn.tsx new file mode 100644 index 000000000..0fae0ad85 --- /dev/null +++ b/src-ts/tools/learn/certification-details/enroll-cta-btn/EnrollCtaBtn.tsx @@ -0,0 +1,52 @@ +import { FC, useCallback, useContext } from 'react' +import { NavigateFunction, useNavigate } from 'react-router-dom' + +import { Button, profileContext, ProfileContextData } from '../../../../lib' +import { getAuthenticateAndEnrollRoute, getTCACertificationEnrollPath } from '../../learn.routes' + +interface EnrollCtaBtnProps { + certification: string +} + +const EnrollCtaBtn: FC = (props: EnrollCtaBtnProps) => { + const navigate: NavigateFunction = useNavigate() + const { initialized: profileReady, profile }: ProfileContextData = useContext(profileContext) + + const isLoggedIn: boolean = profileReady && !!profile + + /** + * Handle user click on start course/resume/login button + */ + const handleEnrollClick: () => void = useCallback(() => { + + // if user is not logged in, redirect to login page + if (!isLoggedIn) { + // add a flag to the return url to show the academic policy modal + // or resume the course when they're back + window.location.href = getAuthenticateAndEnrollRoute() + return + } + + // if the user is wipro and s/he hasn't set up DICE, + // let the user know + if (profile?.isWipro && !profile.diceEnabled) { + // setIsDiceModalOpen(true) + return + } + + navigate(getTCACertificationEnrollPath(props.certification)) + }, [isLoggedIn, profile?.isWipro, profile?.diceEnabled, props, navigate]) + + return ( + <> +
+ +) + +export default EnrollmentSidebar diff --git a/src-ts/tools/learn/certification-details/enrollment-page/enrollment-sidebar/index.ts b/src-ts/tools/learn/certification-details/enrollment-page/enrollment-sidebar/index.ts new file mode 100644 index 000000000..d627429b9 --- /dev/null +++ b/src-ts/tools/learn/certification-details/enrollment-page/enrollment-sidebar/index.ts @@ -0,0 +1 @@ +export { default as EnrollmentSidebar } from './EnrollmentSidebar' diff --git a/src-ts/tools/learn/certification-details/enrollment-page/index.ts b/src-ts/tools/learn/certification-details/enrollment-page/index.ts new file mode 100644 index 000000000..4ed9659e0 --- /dev/null +++ b/src-ts/tools/learn/certification-details/enrollment-page/index.ts @@ -0,0 +1 @@ +export { default as EnrollmentPage } from './EnrollmentPage' diff --git a/src-ts/tools/learn/certification-details/hero-title/HeroTitle.module.scss b/src-ts/tools/learn/certification-details/hero-title/HeroTitle.module.scss new file mode 100644 index 000000000..8a29056c5 --- /dev/null +++ b/src-ts/tools/learn/certification-details/hero-title/HeroTitle.module.scss @@ -0,0 +1,14 @@ +@import '../../../../lib/styles/includes'; + +.wrap { + display: flex; + align-items: flex-start; + gap: $space-xxl; + margin-bottom: $space-lg; +} + +.text { + h1 { + margin-bottom: $space-sm; + } +} diff --git a/src-ts/tools/learn/certification-details/hero-title/HeroTitle.tsx b/src-ts/tools/learn/certification-details/hero-title/HeroTitle.tsx new file mode 100644 index 000000000..16c8825a6 --- /dev/null +++ b/src-ts/tools/learn/certification-details/hero-title/HeroTitle.tsx @@ -0,0 +1,34 @@ +import { FC } from 'react' + +import { + CertificateBadgeIcon, + ProvidersLogoList, + TCACertification, +} from '../../learn-lib' + +import styles from './HeroTitle.module.scss' + +interface HeroTitleProps { + certification: TCACertification + certTitle: string +} + +const HeroTitle: FC = (props: HeroTitleProps) => ( +
+ +
+

+ {props.certTitle} +

+ +
+
+) + +export default HeroTitle diff --git a/src-ts/tools/learn/certification-details/hero-title/index.ts b/src-ts/tools/learn/certification-details/hero-title/index.ts new file mode 100644 index 000000000..77e760e2f --- /dev/null +++ b/src-ts/tools/learn/certification-details/hero-title/index.ts @@ -0,0 +1 @@ +export { default as HeroTitle } from './HeroTitle' diff --git a/src-ts/tools/learn/certification-details/index.ts b/src-ts/tools/learn/certification-details/index.ts new file mode 100644 index 000000000..e9fdcd867 --- /dev/null +++ b/src-ts/tools/learn/certification-details/index.ts @@ -0,0 +1 @@ +export { default as CertificationDetailsPage } from './CertificationDetailsPage' 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 new file mode 100644 index 000000000..326e51bae --- /dev/null +++ b/src-ts/tools/learn/certification-details/page-layout/PageLayout.module.scss @@ -0,0 +1,43 @@ +@import '../../../../lib/styles/includes'; + +.hero-wrap { + :global(.hero-content) { + flex-direction: column; + gap: $space-xxxxl; + position: relative; + + @include gtelg { + padding-right: calc(40vw + $space-xxl); + } + + @media (min-width: 1150px) { + padding-right: calc(445px + $space-xxl); + } + } +} + +.contentWrap { + .outerContentWrap { + padding-top: $space-mxx; + padding-bottom: 120px; + + @include ltexl { + padding-top: $space-xxxxl; + padding-bottom: $space-xxxxl; + } + + @include gtelg { + padding-right: calc(40vw + $space-xxl * 2); + } + + @media (min-width: 1150px) { + padding-right: calc(445px + $space-xxxl * 2); + } + } + + .innerContentWrap.innerContentWrap.innerContentWrap { + @include ltemd { + overflow: visible; + } + } +} \ No newline at end of file diff --git a/src-ts/tools/learn/certification-details/page-layout/PageLayout.tsx b/src-ts/tools/learn/certification-details/page-layout/PageLayout.tsx new file mode 100644 index 000000000..426c1cdc6 --- /dev/null +++ b/src-ts/tools/learn/certification-details/page-layout/PageLayout.tsx @@ -0,0 +1,85 @@ +import { FC, ReactNode } from 'react' + +import { PageSubheaderPortalId } from '../../../../config' +import { + Breadcrumb, + BreadcrumbItemModel, + ContentLayout, + LoadingSpinner, + Portal, + textFormatGetSafeString, +} from '../../../../lib' +import { + TCACertification, + useLearnBreadcrumb, + WaveHero, +} from '../../learn-lib' +import { getTCACertificationPath } from '../../learn.routes' +import { HeroTitle } from '../hero-title' + +import styles from './PageLayout.module.scss' + +interface PageLayoutProps { + certification?: TCACertification + extraBreadCrumbs?: Array + mainContent: ReactNode + heroCTA?: ReactNode + sidebarContents: ReactNode +} + +const PageLayout: FC = (props: PageLayoutProps) => { + + const breadcrumb: Array = useLearnBreadcrumb([ + { + + name: textFormatGetSafeString(props.certification?.title), + url: getTCACertificationPath(props.certification?.dashedName ?? ''), + }, + ...(props.extraBreadCrumbs ?? []), + ]) + + function renderContents(): ReactNode { + if (!props.certification) { + return <> + } + + return ( + <> + + + +
+ + )} + theme='grey' + text={props.certification.introText} + > + {props.heroCTA} + + {props.sidebarContents} +
+
+ + {props.mainContent} + + ) + } + + return ( + + {props.mainContent ? renderContents() : ( +
+ +
+ )} +
+ ) +} + +export default PageLayout diff --git a/src-ts/tools/learn/certification-details/page-layout/index.ts b/src-ts/tools/learn/certification-details/page-layout/index.ts new file mode 100644 index 000000000..0c15d084d --- /dev/null +++ b/src-ts/tools/learn/certification-details/page-layout/index.ts @@ -0,0 +1 @@ +export { default as PageLayout } from './PageLayout' diff --git a/src-ts/tools/learn/certification-details/perks-section/PerksSection.module.scss b/src-ts/tools/learn/certification-details/perks-section/PerksSection.module.scss new file mode 100644 index 000000000..6cc2889b1 --- /dev/null +++ b/src-ts/tools/learn/certification-details/perks-section/PerksSection.module.scss @@ -0,0 +1,71 @@ +@import '../../../../lib/styles/includes'; + +.wrap { + background: $black-5; + padding: $space-xxxxl; + padding-right: 92px; + border-radius: $space-sm; + + color: $black-100; + @include ltelg { + padding: $space-xxl $space-lg; + } + + &.clear { + background: $tc-white; + padding: 0; + } +} + +.perksList { + margin-top: $space-xxxxl; + @include ltelg { + margin-top: $space-xxl; + } + + li { + display: flex; + align-items: flex-start; + gap: $space-lg; + + margin-top: $space-xxl; + @include ltelg { + gap: $space-sm; + } + } +} + +.perkIcon { + display: flex; + width: $space-mxx; + height: $space-mxx; + flex: 0 0 auto; + + @include ltelg { + width: $space-xxxxl; + height: $space-xxxxl; + } + + svg { + display: block; + width: 100%; + height: 100%; + + path { + stroke: url(#paint0_linear_1847_10558); + } + } + + li:nth-child(2) & { + svg path { + stroke: none; + fill: url(#paint0_linear_1847_10558); + } + } +} + +.perkContent { + p { + margin-top: $space-sm; + } +} diff --git a/src-ts/tools/learn/certification-details/perks-section/PerksSection.tsx b/src-ts/tools/learn/certification-details/perks-section/PerksSection.tsx new file mode 100644 index 000000000..cbfdfd663 --- /dev/null +++ b/src-ts/tools/learn/certification-details/perks-section/PerksSection.tsx @@ -0,0 +1,49 @@ +import classNames from 'classnames' +import { FC } from 'react' + +import { type PerkItem } from '../data/perks.data' + +import { getPerkIcon } from './icons-map' +import styles from './PerksSection.module.scss' + +interface PerksSectionProps { + items: Array + title?: string + style?: 'clear' +} + +const PerksSection: FC = (props: PerksSectionProps) => ( +
+

{props.title ?? 'Why certify with Topcoder?'}

+ + + + + + + + +
    + {props.items.map(perk => ( +
  • +
    + {getPerkIcon(perk)} +
    +
    +

    {perk.title}

    +

    {perk.description}

    +
    +
  • + ))} +
+
+) + +export default PerksSection diff --git a/src-ts/tools/learn/certification-details/perks-section/icons-map.tsx b/src-ts/tools/learn/certification-details/perks-section/icons-map.tsx new file mode 100644 index 000000000..dce3536ec --- /dev/null +++ b/src-ts/tools/learn/certification-details/perks-section/icons-map.tsx @@ -0,0 +1,16 @@ +import { ReactNode } from 'react' + +import { IconOutline } from '../../../../lib' +import { IconCertifSvg } from '../../learn-lib' +import { PerkIconsType, PerkItem } from '../data/perks.data' + +export const iconsMap: {[key in PerkIconsType]: ReactNode} = { + 'currency-dolary': , + 'filter-icon': , + 'icon-certif': , + 'shield-check': , +} + +export function getPerkIcon(perk: PerkItem): ReactNode { + return iconsMap[perk.icon] +} diff --git a/src-ts/tools/learn/certification-details/perks-section/index.ts b/src-ts/tools/learn/certification-details/perks-section/index.ts new file mode 100644 index 000000000..ac04ece5a --- /dev/null +++ b/src-ts/tools/learn/certification-details/perks-section/index.ts @@ -0,0 +1 @@ +export { default as PerksSection } from './PerksSection' diff --git a/src-ts/tools/learn/course-certificate/certificate-not-found/CertificateNotFound.module.scss b/src-ts/tools/learn/course-certificate/certificate-not-found/CertificateNotFound.module.scss new file mode 100644 index 000000000..c06d33f00 --- /dev/null +++ b/src-ts/tools/learn/course-certificate/certificate-not-found/CertificateNotFound.module.scss @@ -0,0 +1,91 @@ +@import '../../../../lib/styles/includes'; + +.wrap { + display: flex; + height: 100%; + z-index: 1; + overflow: hidden; + + @include ltemd { + border: 1px dashed $blue-25; + border-radius: 8px; + } + > svg { + position: absolute; + top: 50%; + left: 0; + width: 100%; + height: auto; + display: block; + z-index: -1; + transform: translateY(-50%); + + @include ltemd { + :global(.rect-border) { + display: none; + } + } + } +} + +.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; + } + + h2 { + color: $blue-25; + } + + h3 { + font-size: 48px; + line-height: 50px; + font-weight: 500; + color: $tc-white; + margin-top: $space-sm; + + :global(.nw) { + white-space: nowrap; + } + } +} + +.logos { + margin-top: auto; + display: flex; +} + +.logo { + display: flex; + align-items: center; + height: 52px; + + svg { + width: auto; + + } + &.whiteLogo svg > path { + fill: $tc-white; + } +} + +.divider { + width: $border; + background: $black-10; + margin: 0 $space-lg; + flex: 0 0 auto; +} + +.rightSide { + width: 45%; + +} diff --git a/src-ts/tools/learn/course-certificate/certificate-not-found/CertificateNotFound.tsx b/src-ts/tools/learn/course-certificate/certificate-not-found/CertificateNotFound.tsx new file mode 100644 index 000000000..8bf2a1a89 --- /dev/null +++ b/src-ts/tools/learn/course-certificate/certificate-not-found/CertificateNotFound.tsx @@ -0,0 +1,37 @@ +import { FC } from 'react' +import classNames from 'classnames' + +import { TCAcademyLogoMixedSvg, TcLogoSvg } from '../../../../lib' +import { CertificateNotFoundContent } from '../../learn-lib' + +import { ReactComponent as BackgroundSvg } from './bg.svg' +import styles from './CertificateNotFound.module.scss' + +const CertificateNotFound: FC<{}> = () => ( +
+ +
+
+

Topcoder Academy

+

+ Certificate + {' '} + not found +

+ +
+
+ +
+
+
+ +
+
+
+
+
+
+) + +export default CertificateNotFound diff --git a/src-ts/tools/learn/course-certificate/certificate-not-found/bg.svg b/src-ts/tools/learn/course-certificate/certificate-not-found/bg.svg new file mode 100644 index 000000000..58bebe31b --- /dev/null +++ b/src-ts/tools/learn/course-certificate/certificate-not-found/bg.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/course-certificate/certificate-not-found/index.ts b/src-ts/tools/learn/course-certificate/certificate-not-found/index.ts new file mode 100644 index 000000000..a3ba843a9 --- /dev/null +++ b/src-ts/tools/learn/course-certificate/certificate-not-found/index.ts @@ -0,0 +1 @@ +export { default as CertificateNotFound } from './CertificateNotFound' 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..42569faa1 100644 --- a/src-ts/tools/learn/course-certificate/certificate-view/CertificateView.tsx +++ b/src-ts/tools/learn/course-certificate/certificate-view/CertificateView.tsx @@ -1,49 +1,40 @@ -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' + FC, + MutableRefObject, + ReactNode, + useMemo, + useRef, +} from 'react' + import { AllCertificationsProviderData, + CertificateNotFoundContent, + CertificatePageLayout, CoursesProviderData, useGetCertification, useGetCourses, useGetUserCompletedCertifications, UserCompletedCertificationsProviderData, } 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' +import { + getCoursePath, + getUserCertificateSsr, +} from '../../learn.routes' +import { UserProfile } from '../../../../lib' +import { CertificateNotFound } from '../certificate-not-found' -export type CertificateViewStyle = 'large-container' | undefined +import Certificate from './certificate/Certificate' interface CertificateViewProps { - certification: string, - hideActions?: boolean, - onCertificationNotCompleted: () => void - profile: UserProfile, - provider: string, - viewStyle: CertificateViewStyle + certification: string + fullScreenCertLayout?: boolean + profile: UserProfile + provider: string } const CertificateView: FC = (props: CertificateViewProps) => { - - const navigate: NavigateFunction = useNavigate() const coursePath: string = getCoursePath(props.provider, props.certification) const certificateElRef: MutableRefObject = useRef() - const certificateWrapRef: MutableRefObject = useRef() const userName: string = useMemo(() => ( [props.profile.firstName, props.profile.lastName].filter(Boolean) @@ -56,13 +47,22 @@ const CertificateView: FC = (props: CertificateViewProps) ready: courseReady, }: CoursesProviderData = useGetCourses(props.provider, props.certification) + const { + certification: certificate, + ready: certificateReady, + }: AllCertificationsProviderData = useGetCertification( + props.provider, + course?.certificationId ?? '', + { enabled: !!course?.certificationId }, + ) + function getCertTitle(user: string): string { return `${user} - ${course?.title} Certification` } const certUrl: string = getUserCertificateSsr( props.provider, - props.certification, + certificate?.certification ?? '', props.profile.handle, getCertTitle(props.profile.handle), ) @@ -75,146 +75,50 @@ const CertificateView: FC = (props: CertificateViewProps) }: UserCompletedCertificationsProviderData = useGetUserCompletedCertifications( props.profile.userId, props.provider, - props.certification, + certificate?.certification, ) const hasCompletedTheCertification: boolean = !!completedCertificate - const { - certification: certificate, - ready: certificateReady, - }: AllCertificationsProviderData = useGetCertification( - props.provider, - course?.certificationId ?? '', - { enabled: !!course?.certificationId }, - ) - const ready: boolean = useMemo(() => ( completedCertificateReady && courseReady && certificateReady ), [certificateReady, completedCertificateReady, courseReady]) + const certificateNotFoundError: boolean = ready && !hasCompletedTheCertification - const readyAndCompletedCertification: boolean = useMemo(() => ( - ready && hasCompletedTheCertification - ), [hasCompletedTheCertification, ready]) - - useCertificateScaling(ready ? certificateWrapRef : undefined) - - const handleBackBtnClick: () => void = useCallback(() => { - 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 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(() => { + function renderCertificate(): ReactNode { if (ready && !hasCompletedTheCertification) { - props.onCertificationNotCompleted() + return } - }, [coursePath, hasCompletedTheCertification, props, ready]) + + return ( + + ) + } return ( - <> - - - {ready && readyAndCompletedCertification && ( -
-
- {!props.hideActions && ( -
- } - onClick={handleBackBtnClick} - /> -
- )} -
- -
- {!props.hideActions && ( -
- } - onClick={handlePrint} - /> - } - onClick={handleDownload} - /> - - - -
- )} -
-
+ )} - + > + {renderCertificate()} + ) } diff --git a/src-ts/tools/learn/course-certificate/certificate-view/certificate/Certificate.module.scss b/src-ts/tools/learn/course-certificate/certificate-view/certificate/Certificate.module.scss index 1ea492c07..fc8f51baa 100644 --- a/src-ts/tools/learn/course-certificate/certificate-view/certificate/Certificate.module.scss +++ b/src-ts/tools/learn/course-certificate/certificate-view/certificate/Certificate.module.scss @@ -1,9 +1,9 @@ @import '../../../../../lib/styles/includes'; -@import './includes'; .wrap { display: flex; height: 100%; + background: $tc-white; } .details { @@ -20,7 +20,7 @@ flex-direction: column; } - .wrap:global(.large-container) & { + :global(.large-container) & { padding-left: calc($space-mx + $space-mxx); } @@ -137,14 +137,14 @@ svg { width: auto; - height: 100%; } } .divider { width: $border; background: $black-10; - margin: 0 $space-xxxxl; + margin: 0 $space-lg; + flex: 0 0 auto; } .vendor { diff --git a/src-ts/tools/learn/course-certificate/certificate-view/certificate/Certificate.tsx b/src-ts/tools/learn/course-certificate/certificate-view/certificate/Certificate.tsx index 2071dff61..dfccc2d74 100644 --- a/src-ts/tools/learn/course-certificate/certificate-view/certificate/Certificate.tsx +++ b/src-ts/tools/learn/course-certificate/certificate-view/certificate/Certificate.tsx @@ -3,12 +3,10 @@ import classNames from 'classnames' import { LearnConfig } from '../../../learn-config' import { LearnCertificateTrackType } from '../../../learn-lib' +import { FccLogoSvg, TcAcademyLogoSvg, TcLogoSvg } from '../../../../../lib' import { CertificateBgPattern } from './certificate-bg-pattern' import { CourseCard } from './course-card' -import { ReactComponent as TcAcademyLogoSvg } from './tc-academy-logo.svg' -import { ReactComponent as TcLogoSvg } from './tc-logo.svg' -import { ReactComponent as FccLogoSvg } from './vendor-fcc-logo.svg' import styles from './Certificate.module.scss' interface CertificateProps { diff --git a/src-ts/tools/learn/course-certificate/certificate-view/certificate/certificate-bg-pattern/CertificateBgPattern.module.scss b/src-ts/tools/learn/course-certificate/certificate-view/certificate/certificate-bg-pattern/CertificateBgPattern.module.scss index 02c4c5bec..41cf84566 100644 --- a/src-ts/tools/learn/course-certificate/certificate-view/certificate/certificate-bg-pattern/CertificateBgPattern.module.scss +++ b/src-ts/tools/learn/course-certificate/certificate-view/certificate/certificate-bg-pattern/CertificateBgPattern.module.scss @@ -1,4 +1,4 @@ -@import '../includes'; +@import '../../../../../../lib/styles/includes'; @mixin wave-bg-pattern($grad) { background: url('./wave-bg-2.png') 0 0 no-repeat, diff --git a/src-ts/tools/learn/course-certificate/certificate-view/certificate/includes.scss b/src-ts/tools/learn/course-certificate/certificate-view/certificate/includes.scss deleted file mode 100644 index 6db0d3dd5..000000000 --- a/src-ts/tools/learn/course-certificate/certificate-view/certificate/includes.scss +++ /dev/null @@ -1,17 +0,0 @@ -$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/course-certificate/my-certificate/MyCertificate.tsx b/src-ts/tools/learn/course-certificate/my-certificate/MyCertificate.tsx index c9d20a18c..644d414bf 100644 --- a/src-ts/tools/learn/course-certificate/my-certificate/MyCertificate.tsx +++ b/src-ts/tools/learn/course-certificate/my-certificate/MyCertificate.tsx @@ -37,7 +37,6 @@ const MyCertificate: FC<{}> = () => { certification={certificationParam} profile={profile} provider={providerParam} - onCertificationNotCompleted={navigateToCourse} /> )} diff --git a/src-ts/tools/learn/course-certificate/user-certificate/UserCertificate.module.scss b/src-ts/tools/learn/course-certificate/user-certificate/UserCertificate.module.scss deleted file mode 100644 index 06821e91a..000000000 --- a/src-ts/tools/learn/course-certificate/user-certificate/UserCertificate.module.scss +++ /dev/null @@ -1,5 +0,0 @@ -.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/course-certificate/user-certificate/UserCertificate.tsx b/src-ts/tools/learn/course-certificate/user-certificate/UserCertificate.tsx index 6be68de30..7218dc738 100644 --- a/src-ts/tools/learn/course-certificate/user-certificate/UserCertificate.tsx +++ b/src-ts/tools/learn/course-certificate/user-certificate/UserCertificate.tsx @@ -1,21 +1,16 @@ -import { Dispatch, FC, MutableRefObject, SetStateAction, useEffect, useLayoutEffect, useRef, useState } from 'react' -import { Params, useParams, useSearchParams } from 'react-router-dom' +import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react' +import { Params, useParams } 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' +import { CertificateView } from '../certificate-view' const UserCertificate: FC<{}> = () => { - const wrapElRef: MutableRefObject = useRef() const routeParams: Params = useParams() - const [queryParams]: [URLSearchParams, any] = useSearchParams() const [profile, setProfile]: [ UserProfile | undefined, @@ -36,35 +31,17 @@ const UserCertificate: FC<{}> = () => { } }, [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} - /> -
+ )} ) diff --git a/src-ts/tools/learn/course-completed/CourseCompletedPage.tsx b/src-ts/tools/learn/course-completed/CourseCompletedPage.tsx index a856a0ba4..df70a44df 100755 --- a/src-ts/tools/learn/course-completed/CourseCompletedPage.tsx +++ b/src-ts/tools/learn/course-completed/CourseCompletedPage.tsx @@ -1,4 +1,4 @@ -import { FC, useContext, useEffect } from 'react' +import { FC, ReactNode, useContext, useEffect } from 'react' import { NavigateFunction, Params, useNavigate, useParams } from 'react-router-dom' import { EnvironmentConfig } from '../../../config' @@ -15,12 +15,15 @@ import { AllCertificationsProviderData, CoursesProviderData, CourseTitle, + TCACertificationCheckCompleted, useGetCertification, useGetCourses, useGetUserCertificationProgress, useLearnBreadcrumb, UserCertificationProgressProviderData, UserCertificationProgressStatus, + useTCACertificationCheckCompleted, + useTcaCertificationModal, } from '../learn-lib' import { getCertificatePath, getCoursePath, LEARN_PATHS, rootRoute } from '../learn.routes' @@ -55,12 +58,18 @@ const CourseCompletedPage: FC<{}> = () => { ready: certifReady, }: AllCertificationsProviderData = useGetCertification( providerParam, - textFormatGetSafeString(progress?.certificationId), + textFormatGetSafeString(progress?.fccCertificationId), { - enabled: progressReady && !!progress?.certificationId, + enabled: progressReady && !!progress?.fccCertificationId, }, ) + const { certification: tcaCertificationName }: TCACertificationCheckCompleted = useTCACertificationCheckCompleted( + 'FccCertificationProgress', + progress?.id ?? '', + { enabled: !!progress?.id }, + ) + const isLoggedIn: boolean = profileReady && !!profile const certificatesDataReady: boolean = progressReady && certifReady const ready: boolean = profileReady && courseDataReady && (!isLoggedIn || certificatesDataReady) @@ -76,6 +85,8 @@ const CourseCompletedPage: FC<{}> = () => { }, ]) + const tcaCertificationCompletedModal: ReactNode = useTcaCertificationModal(tcaCertificationName) + useEffect(() => { if (ready && progress?.status !== UserCertificationProgressStatus.completed) { navigate(coursePath) @@ -107,8 +118,8 @@ const CourseCompletedPage: FC<{}> = () => {

@@ -125,7 +136,12 @@ const CourseCompletedPage: FC<{}> = () => { size='sm' buttonStyle='secondary' label='View certificate' - route={getCertificatePath(courseData.provider, courseData.certification)} + route={( + getCertificatePath( + courseData.resourceProvider.name, + certificationParam, + ) + )} />
+ {tcaCertificationCompletedModal} )} diff --git a/src-ts/tools/learn/course-details/CourseDetailsPage.module.scss b/src-ts/tools/learn/course-details/CourseDetailsPage.module.scss index 605a7afe9..81e833f5b 100644 --- a/src-ts/tools/learn/course-details/CourseDetailsPage.module.scss +++ b/src-ts/tools/learn/course-details/CourseDetailsPage.module.scss @@ -21,6 +21,10 @@ } } +.tcaCertBanner { + margin-top: $space-xxxxl; +} + .aside { flex: 0 0 auto; max-width: 560px; @@ -36,7 +40,7 @@ flex: 1 1 auto; .text { @extend .body-main; - margin-top: $space-xxl; + margin-top: $space-xxxxl; } } @@ -62,6 +66,7 @@ } .credits-link { + margin-top: $space-xxxxl; padding-bottom: $space-xxl; a { diff --git a/src-ts/tools/learn/course-details/CourseDetailsPage.tsx b/src-ts/tools/learn/course-details/CourseDetailsPage.tsx index ef9a2836b..1cbe919c0 100644 --- a/src-ts/tools/learn/course-details/CourseDetailsPage.tsx +++ b/src-ts/tools/learn/course-details/CourseDetailsPage.tsx @@ -17,6 +17,7 @@ import { CoursesProviderData, CourseTitle, ResourceProviderData, + TCACertificationProgressBox, useGetCertification, useGetCourses, useGetResourceProvider, @@ -28,7 +29,6 @@ import { import { getCoursePath } from '../learn.routes' import { CourseCurriculum } from './course-curriculum' -import { PromoCourse } from './promo-course' import styles from './CourseDetailsPage.module.scss' const CourseDetailsPage: FC<{}> = () => { @@ -45,16 +45,6 @@ const CourseDetailsPage: FC<{}> = () => { ready: courseReady, }: CoursesProviderData = useGetCourses(textFormatGetSafeString(routeParams.provider), routeParams.certification) - const { - certificationProgress: progress, - ready: progressReady, - setCertificateProgress, - }: UserCertificationProgressProviderData = useGetUserCertificationProgress( - profile?.userId, - routeParams.provider, - routeParams.certification, - ) - const { certification: certificate, ready: certificateReady, @@ -66,6 +56,16 @@ const CourseDetailsPage: FC<{}> = () => { }, ) + const { + certificationProgress: progress, + ready: progressReady, + setCertificateProgress, + }: UserCertificationProgressProviderData = useGetUserCertificationProgress( + profile?.userId, + routeParams.provider, + certificate?.certification, + ) + const ready: boolean = profileReady && courseReady && certificateReady && (!profile || progressReady) const breadcrumb: Array = useLearnBreadcrumb([ @@ -93,7 +93,7 @@ const CourseDetailsPage: FC<{}> = () => { {' '} {course.title} , - we&appos;d recommend you enroll in another course to continue your learning. + we'd recommend you enroll in another course to continue your learning. You can view our other courses from the Topcoder Academy course page.

@@ -155,7 +155,7 @@ const CourseDetailsPage: FC<{}> = () => { ) } - function getFooter(): ReactNode { + function getProviderCredits(): ReactNode { if (!resourceProvider) { return undefined @@ -183,15 +183,21 @@ const CourseDetailsPage: FC<{}> = () => {
)} - {ready && course && ( + {ready && course && certificate && ( <>
+ +
= () => { {getDescription()} {getPrerequisites()} {getCompletionSuggestion()} -
- -
+ {getProviderCredits()}
= () => { />
- {getFooter()} )} diff --git a/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx b/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx index c6ee2a0a5..212974ace 100644 --- a/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx +++ b/src-ts/tools/learn/course-details/course-curriculum/CourseCurriculum.tsx @@ -1,8 +1,8 @@ +import { noop } from 'lodash' import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react' import { NavigateFunction, useNavigate, useSearchParams } from 'react-router-dom' -import classNames from 'classnames' -import { Button, UserProfile } from '../../../../lib' +import { UserProfile } from '../../../../lib' import { CourseOutline, LearnCourse, @@ -28,6 +28,7 @@ import { DiceModal } from './dice-modal' import styles from './CourseCurriculum.module.scss' interface CourseCurriculumProps { + certification: string course: LearnCourse profile?: UserProfile progress?: LearnUserCertificationProgress @@ -49,7 +50,7 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp const status: string = props.progress?.status ?? UserCertificationProgressStatus.inititialized const completedPercentage: number = (props.progress?.courseProgressPercentage ?? 0) / 100 - const inProgress: boolean = status === UserCertificationProgressStatus.inProgress || !!props.progress?.currentLesson + const inProgress: boolean = status === UserCertificationProgressStatus.inProgress const isCompleted: boolean = status === UserCertificationProgressStatus.completed /** @@ -63,15 +64,16 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp const lesson: LearnLesson = module.lessons[0] const lessonPath: string = getLessonPathFromCurrentLesson( - course.provider, - course.certification, + course.resourceProvider.name, + props.certification, props.progress?.currentLesson, - module.meta.dashedName, + module.dashedName, lesson.dashedName, ) navigate(lessonPath) }, [ navigate, + props.certification, props.course, props.progress, ]) @@ -121,26 +123,26 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp return } - if (!props.progress?.id) { - const progress: LearnUserCertificationProgress = await userCertificationProgressStartAsync( - props.profile.userId, - props.course.certificationId, - props.course.id, - { - lesson: props.course.modules[0].lessons[0].dashedName, - module: props.course.modules[0].meta.dashedName, - }, - ) - - // update progress with data returned from calling the start progress endpoint - props.setCertificateProgress(progress) - } else { - await userCertificationProgressUpdateAsync( - props.progress.id, - UserCertificationUpdateProgressActions.acceptHonestyPolicy, - {}, - ) - } + let progress: LearnUserCertificationProgress | undefined = props.progress + // start and mark progress object as "in progress" + progress = await userCertificationProgressStartAsync( + props.profile.userId, + props.course.certificationId, + props.course.id, + { + lesson: props.course.modules[0].lessons[0].dashedName, + module: props.course.modules[0].dashedName, + }, + ) + + progress = await userCertificationProgressUpdateAsync( + progress!.id, + UserCertificationUpdateProgressActions.acceptHonestyPolicy, + {}, + ) + + // update progress with data returned from calling the start progress endpoint + props.setCertificateProgress(progress) handleStartCourse() // eslint-disable-next-line react-hooks/exhaustive-deps @@ -153,8 +155,11 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp props.progress?.id, ]) - const handleNavigateToCertificate: () => void = () => { - const certificatePath: string = getCertificatePath(props.course.provider, props.course.certification) + function handleNavigateToCertificate(): void { + const certificatePath: string = getCertificatePath( + props.course.resourceProvider.name, + props.certification, + ) navigate(certificatePath) } @@ -203,21 +208,14 @@ const CourseCurriculum: FC = (props: CourseCurriculumProp
- {isCompleted && ( -
-
- )} = (props: CurriculumSummaryP
{title}
- + )}
diff --git a/src-ts/tools/learn/course-details/promo-course/PromoCourse.module.scss b/src-ts/tools/learn/course-details/promo-course/PromoCourse.module.scss deleted file mode 100644 index 284f61825..000000000 --- a/src-ts/tools/learn/course-details/promo-course/PromoCourse.module.scss +++ /dev/null @@ -1,44 +0,0 @@ -@use '../../../../lib/styles/typography'; -@import '../../../../lib/styles/includes'; - -.wrap { - background: $black-5; - border-radius: $space-sm; - - padding: $space-xxxxl; - gap: $space-xxxxl; - display: flex; - flex-direction: column; - @include ltemd { - padding: $space-lg; - gap: $space-sm; - } -} - -.text-content { - > *:not(:first-child) { - margin-top: $space-sm; - } - - h4 { - color: $blue-140; - } -} - -.course-path-steps { - margin-top: $space-sm; - svg { - width: 100%; - display: block; - &:nth-child(2) { - height: 100px; - } - } - - @include ltemd { - display: flex; - flex-direction: column; - gap: $space-lg; - align-items: stretch; - } -} \ No newline at end of file diff --git a/src-ts/tools/learn/course-details/promo-course/PromoCourse.tsx b/src-ts/tools/learn/course-details/promo-course/PromoCourse.tsx deleted file mode 100644 index 16d3932b5..000000000 --- a/src-ts/tools/learn/course-details/promo-course/PromoCourse.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { FC } from 'react' -import classNames from 'classnames' - -import { ReactComponent as LearnGetCertified } from './learn-get-certified.svg' -import { ReactComponent as MobileLearnGetCertified2 } from './mobile-get-certified-2.svg' -import { ReactComponent as MobileLearnGetCertified } from './mobile-get-certified.svg' -import styles from './PromoCourse.module.scss' - -const PromoCourse: FC<{}> = () => ( -
-
-

Coming soon

-
- More ways to reach your potential with Recommended Learning Paths -
-
- We will be building additional learning path courses, where when taken - in sequence, will result in a larger Topcoder certification. - These certifications will show in your Topcoder profile and will showcase - your verified skills and earned certifications. The resulting outcome is - that you have gained essential skills allowing you to be more successful - on the Topcoder platform. -
-
-
- -
-
- - -
-
-) - -export default PromoCourse diff --git a/src-ts/tools/learn/course-details/promo-course/index.ts b/src-ts/tools/learn/course-details/promo-course/index.ts deleted file mode 100644 index 2a71dac9a..000000000 --- a/src-ts/tools/learn/course-details/promo-course/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as PromoCourse } from './PromoCourse' diff --git a/src-ts/tools/learn/course-details/promo-course/learn-get-certified.svg b/src-ts/tools/learn/course-details/promo-course/learn-get-certified.svg deleted file mode 100755 index 5b968b348..000000000 --- a/src-ts/tools/learn/course-details/promo-course/learn-get-certified.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src-ts/tools/learn/course-details/promo-course/mobile-get-certified-2.svg b/src-ts/tools/learn/course-details/promo-course/mobile-get-certified-2.svg deleted file mode 100644 index 92336de67..000000000 --- a/src-ts/tools/learn/course-details/promo-course/mobile-get-certified-2.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/src-ts/tools/learn/course-details/promo-course/mobile-get-certified.svg b/src-ts/tools/learn/course-details/promo-course/mobile-get-certified.svg deleted file mode 100644 index fbdcbdea1..000000000 --- a/src-ts/tools/learn/course-details/promo-course/mobile-get-certified.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx b/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx index 725bfe76a..9cc04bebf 100644 --- a/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx +++ b/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx @@ -96,16 +96,15 @@ const FreeCodeCamp: FC<{}> = () => { const ready: boolean = profileReady && courseDataReady && lessonReady && (!isLoggedIn || progressReady) - const certification: string = textFormatGetSafeString(lesson?.course.certification) const module: string = textFormatGetSafeString(lesson?.module.title) const breadcrumb: Array = useLearnBreadcrumb([ { name: textFormatGetSafeString(lesson?.course.title), - url: getCoursePath(providerParam, certification), + url: getCoursePath(providerParam, certificationParam), }, { name: module, - url: getLessonPathFromModule(providerParam, certification, module, lessonParam), + url: getLessonPathFromModule(providerParam, certificationParam, module, lessonParam), }, ]) @@ -176,8 +175,19 @@ const FreeCodeCamp: FC<{}> = () => { .reverse() updatePath(nLessonPath, modulePath, coursePath) + const newLessonId: string = ((): string => { + if (!courseData) return '' + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const newModule: LearnModule = courseData.modules.find(m => m.dashedName === modulePath)! + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const newLesson: LearnLesson = newModule.lessons.find(l => l.dashedName === nLessonPath)! + return newLesson.id + })() + const currentLesson: { [key: string]: string } = { lesson: nLessonPath, + lessonId: newLessonId, module: modulePath, } @@ -254,7 +264,7 @@ const FreeCodeCamp: FC<{}> = () => { function getModuleFromProgress(certProgress: LearnUserCertificationProgress): LearnModuleProgress | undefined { - return certProgress.modules.find(m => m.module === moduleParam) + return certProgress.moduleProgresses.find(m => m.module === moduleParam) } function handleSurvey(certWasInProgress: boolean, progress: LearnUserCertificationProgress): void { @@ -271,7 +281,7 @@ const FreeCodeCamp: FC<{}> = () => { } // if there are any other modules that have been completed, there's nothing to do - if (progress.modules + if (progress.moduleProgresses .some(m => m.module !== moduleParam && m.moduleStatus === LearnModuleStatus.completed) ) { return @@ -313,7 +323,7 @@ const FreeCodeCamp: FC<{}> = () => { // so we find the first incomplete lesson // and redirect user to it for a continuous flow const firstIncompleteModule: LearnModuleProgress | undefined - = certificateProgress.modules.find(m => m.completedPercentage !== 100) + = certificateProgress.moduleProgresses.find(m => m.completedPercentage !== 100) const moduleLessons: Array | undefined = courseData?.modules.find(m => m.key === firstIncompleteModule?.module)?.lessons if (!firstIncompleteModule || !moduleLessons) { @@ -458,6 +468,7 @@ const FreeCodeCamp: FC<{}> = () => { // redirect the user to course details page to perform the // necessary actions + const coursePath: string = getCoursePath( providerParam, certificationParam, @@ -516,16 +527,18 @@ const FreeCodeCamp: FC<{}> = () => { {lesson && (
= (props: FccFrameProps) => { const frameRef: MutableRefObject = useRef() const frameIsReady: MutableRefObject = useRef(false) - const { onFccLastLessonNavigation, onFccLessonChange, onFccLessonComplete }: FccFrameProps = props const lessonUrl: string | undefined = props.lesson?.lessonUrl useEffect(() => { @@ -47,7 +47,7 @@ const FccFrame: FC = (props: FccFrameProps) => { useEffect(() => { if (!frameRef) { - return + return noop } const handleEvent: (event: any) => void = (event: any) => { @@ -66,28 +66,28 @@ const FccFrame: FC = (props: FccFrameProps) => { } = JSON.parse(jsonData) if (eventName === 'fcc:nav:last-challenge') { - onFccLastLessonNavigation() + props.onFccLastLessonNavigation.call(undefined) } if (eventName === 'fcc:challenge:completed') { - onFccLessonComplete(data?.meta?.id) + props.onFccLessonComplete.call(undefined, data?.meta?.id) } if (eventName === 'fcc:challenge:ready') { frameIsReady.current = true - onFccLessonChange(data.path) + props.onFccLessonChange.call(undefined, data.path) } } window.addEventListener('message', handleEvent, false) - return () => { + return (): void => { window.removeEventListener('message', handleEvent, false) } }, [ frameRef, - onFccLastLessonNavigation, - onFccLessonChange, - onFccLessonComplete, + props.onFccLastLessonNavigation, + props.onFccLessonChange, + props.onFccLessonComplete, ]) return ( diff --git a/src-ts/tools/learn/free-code-camp/fcc-sidebar/FccSidebar.module.scss b/src-ts/tools/learn/free-code-camp/fcc-sidebar/FccSidebar.module.scss index 789554d72..10b70ceb8 100644 --- a/src-ts/tools/learn/free-code-camp/fcc-sidebar/FccSidebar.module.scss +++ b/src-ts/tools/learn/free-code-camp/fcc-sidebar/FccSidebar.module.scss @@ -28,4 +28,5 @@ @extend .body-main-bold; flex: 0 0 auto; margin-bottom: $space-xl; + margin-top: $space-lg; } \ No newline at end of file diff --git a/src-ts/tools/learn/free-code-camp/fcc-sidebar/FccSidebar.tsx b/src-ts/tools/learn/free-code-camp/fcc-sidebar/FccSidebar.tsx index c298575f2..e5b5f8881 100644 --- a/src-ts/tools/learn/free-code-camp/fcc-sidebar/FccSidebar.tsx +++ b/src-ts/tools/learn/free-code-camp/fcc-sidebar/FccSidebar.tsx @@ -1,27 +1,39 @@ -import { FC, useState } from 'react' +import { Dispatch, FC, SetStateAction, useState } from 'react' -import { CollapsiblePane, CourseOutline, LearnCourse, LearnUserCertificationProgress } from '../../learn-lib' +import { + CollapsiblePane, + CourseOutline, + LearnCourse, + LearnUserCertificationProgress, + TCACertificationProgressBox, +} from '../../learn-lib' import styles from './FccSidebar.module.scss' interface FccSidebarProps { + certification: string certificateProgress?: LearnUserCertificationProgress courseData?: LearnCourse courseDataReady: boolean currentStep: string refetchProgress: () => void + userId?: number } const FccSidebar: FC = (props: FccSidebarProps) => { const [isOpen, setIsOpen]: [boolean, Dispatch>] = useState(false) - const handleToggle: (isOutlineOpen: boolean) => void = (isOutlineOpen: boolean) => { + function handleToggle(isOutlineOpen: boolean): void { setIsOpen(isOutlineOpen) if (isOutlineOpen) { props.refetchProgress() } } + function toggle(): void { + setIsOpen(false) + } + return (
= (props: FccSidebarProps) => { isOpen={isOpen} >
+
{props.courseData?.title}
setIsOpen(false)} + onItemNavigate={toggle} />
diff --git a/src-ts/tools/learn/learn-config/learn.default.config.ts b/src-ts/tools/learn/learn-config/learn.default.config.ts index 066ada8dc..a5ea5c1fd 100644 --- a/src-ts/tools/learn/learn-config/learn.default.config.ts +++ b/src-ts/tools/learn/learn-config/learn.default.config.ts @@ -1,7 +1,7 @@ import { LearnConfigModel } from './learn-config.model' export const LearnConfigDefault: LearnConfigModel = { - API: 'http://localhost:3001/v5/learning-paths', + API: 'https://api.topcoder-dev.com/v5/learning-paths', CERT_ALT_PARAMS: { 'view-style': 'large-container', }, 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 74% 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..20e3ebc3d 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; @@ -18,4 +18,8 @@ svg { @include icon-xxl; } + + @include ltemd { + @include icon-mx; + } } 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/certificate-not-found-content/CertificateNotFoundContent.module.scss b/src-ts/tools/learn/learn-lib/certificate-not-found-content/CertificateNotFoundContent.module.scss new file mode 100644 index 000000000..0313f758a --- /dev/null +++ b/src-ts/tools/learn/learn-lib/certificate-not-found-content/CertificateNotFoundContent.module.scss @@ -0,0 +1,43 @@ +@import '../../../../lib/styles/includes'; + +.content { + color: $blue-25; + margin-top: $space-lg; + + @include ltemd { + padding-left: $space-xxl; + padding-right: $space-xxl; + } + + @include ltesm { + padding-left: $space-lg; + padding-right: $space-lg; + } + + p, ol, li { + margin-top: $space-lg; + } + ol { + list-style: decimal; + padding-left: 16px; + } + + ol li a { + display: flex; + align-items: center; + gap: $space-xs; + font-family: $font-roboto; + font-weight: bold; + font-size: 14px; + line-height: 14px; + letter-spacing: 0.03em; + text-transform: uppercase; + color: $tc-white; + margin-top: $space-sm; + svg { + display: block; + width: 14px; + height: 14px; + } + } +} diff --git a/src-ts/tools/learn/learn-lib/certificate-not-found-content/CertificateNotFoundContent.tsx b/src-ts/tools/learn/learn-lib/certificate-not-found-content/CertificateNotFoundContent.tsx new file mode 100644 index 000000000..984be99fb --- /dev/null +++ b/src-ts/tools/learn/learn-lib/certificate-not-found-content/CertificateNotFoundContent.tsx @@ -0,0 +1,41 @@ +import { FC } from 'react' +import classNames from 'classnames' + +import { IconSolid } from '../../../../lib' + +import styles from './CertificateNotFoundContent.module.scss' + +interface CertificateNotFoundContentProps { + className?: string +} + +const CertificateNotFoundContent: FC = (props: CertificateNotFoundContentProps) => ( +
+

+ Hey there! +

+

+ Looks like we don’t have your certificate. + Have you completed the course? If not, keep going at it! If you have: +

+
    +
  1. + Try again in 60 seconds. We might still be generating your certification! +
  2. +
  3. + If you already waited, contact our support team. + Please reference Topcoder Academy and the course you completed. + + + Contact support + + +
  4. +
+

+ Keep Learning away! +

+
+) + +export default CertificateNotFoundContent diff --git a/src-ts/tools/learn/learn-lib/certificate-not-found-content/index.ts b/src-ts/tools/learn/learn-lib/certificate-not-found-content/index.ts new file mode 100644 index 000000000..ce26ba074 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/certificate-not-found-content/index.ts @@ -0,0 +1 @@ +export { default as CertificateNotFoundContent } from './CertificateNotFoundContent' diff --git a/src-ts/tools/learn/course-certificate/certificate-view/CertificateView.module.scss b/src-ts/tools/learn/learn-lib/certificate-page-layout/CertificatePageLayout.module.scss similarity index 50% rename from src-ts/tools/learn/course-certificate/certificate-view/CertificateView.module.scss rename to src-ts/tools/learn/learn-lib/certificate-page-layout/CertificatePageLayout.module.scss index d9b0568b9..549acc424 100644 --- a/src-ts/tools/learn/course-certificate/certificate-view/CertificateView.module.scss +++ b/src-ts/tools/learn/learn-lib/certificate-page-layout/CertificatePageLayout.module.scss @@ -7,6 +7,7 @@ display: flex; background: $tc-grad15; + overflow: hidden; } .content-wrap { @@ -21,6 +22,7 @@ @include ltemd { flex-direction: column; margin: 0 auto auto; + gap: $space-xxl; } } @@ -28,6 +30,8 @@ display: flex; flex-direction: column; align-items: center; + z-index: 1; + position: relative; gap: $space-sm; @@ -38,22 +42,56 @@ @include ltemd { flex-direction: row; &:last-child { - justify-content: center; + justify-content: space-between; } } + + &.disabled { + opacity: 0.3; + pointer-events: none; + } } .certificate-wrap { - aspect-ratio: 1.25715; - width: 880px; - - background: #fff; - - box-shadow: 0 20px 36px rgba($tc-black, 0.22); + aspect-ratio: $tca-certif-aspect-ratio; + width: 100%; + max-width: 1216px; + position: relative; &:global(.large-container) { aspect-ratio: unset; @include socialPreviewImg; + max-width: none; + } + + body:global(.canvas-clone) & { + transform: none!important; + } +} + +.certifInnerWrap { + position: absolute; + top: 0; + left: 0; + width: 880px; + aspect-ratio: $tca-certif-aspect-ratio; + + box-shadow: 0 20px 36px rgba($tc-black, 0.22); + + .wrap:global(.cert-not-found-layout) & { + box-shadow: none; + } + + > * { + position: absolute; + top: 0; + left: 0; + width: 100%; + } + + .certificate-wrap:global(.large-container) & { + width: 100%; + height: 100%; } } @@ -77,4 +115,27 @@ svg { @include icon-xxl; } + + @include ltemd { + @include icon-mx; + } +} + +// Certificate not found layout +.wrap:global(.cert-not-found-layout) { + @include ltemd { + flex-direction: column; + + .certificate-wrap, .certifInnerWrap { + aspect-ratio: 2.09146; + } + + .content-wrap { + margin-bottom: 0; + } + } + + .certifInnerWrap { + box-shadow: none; + } } diff --git a/src-ts/tools/learn/learn-lib/certificate-page-layout/CertificatePageLayout.tsx b/src-ts/tools/learn/learn-lib/certificate-page-layout/CertificatePageLayout.tsx new file mode 100644 index 000000000..297c922a5 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/certificate-page-layout/CertificatePageLayout.tsx @@ -0,0 +1,156 @@ +import { + FC, + MutableRefObject, + ReactNode, + useCallback, + useLayoutEffect, + useRef, +} from 'react' +import { useSearchParams } from 'react-router-dom' +import classNames from 'classnames' + +import { + FacebookSocialShareBtn, + fileDownloadCanvasAsImage, + IconOutline, + LinkedinSocialShareBtn, + LoadingSpinner, + NavigateBackFunction, + TwitterSocialShareBtn, + useNavigateBack, +} from '../../../../lib' +import { useCertificateScaling } from '../use-certificate-scaling-hook' +import { useCertificateCanvas } from '../use-certificate-canvas-hook' +import { useCertificatePrint } from '../use-certificate-print-hook' +import { ActionButton } from '../action-button' +import { hideSiblings } from '../functions' +import { getViewStyleParamKey } from '../../learn.routes' + +import styles from './CertificatePageLayout.module.scss' + +export type CertificatePageLayoutStyle = 'large-container' + +interface CertificatePageLayoutProps { + actions?: ReactNode + certificateElRef: MutableRefObject + children?: ReactNode + afterContent?: ReactNode + className?: string + disableActions?: boolean + fallbackBackUrl?: string + fullScreenCertLayout?: boolean + isCertificateCompleted?: boolean + isReady?: boolean + ssrUrl: string + title?: string +} + +const CertificatePageLayout: FC = (props: CertificatePageLayoutProps) => { + const [queryParams]: [URLSearchParams, any] = useSearchParams() + const viewStyle: CertificatePageLayoutStyle = queryParams.get(getViewStyleParamKey()) as CertificatePageLayoutStyle + + const wrapElRef: MutableRefObject = useRef() + const certificateWrapRef: MutableRefObject = useRef() + const navigateBack: NavigateBackFunction = useNavigateBack() + + useCertificateScaling( + props.isReady ? certificateWrapRef : undefined, + 880, + 880, + viewStyle ? 1 : Math.min(), + ) + + const handleBackBtnClick: () => void = useCallback(() => { + navigateBack(props.fallbackBackUrl ?? '') + }, [props.fallbackBackUrl, navigateBack]) + + const getCertificateCanvas: () => Promise + = useCertificateCanvas(props.certificateElRef) + + const handleDownload: () => Promise = useCallback(async () => { + + const canvas: HTMLCanvasElement | void = await getCertificateCanvas() + if (!!canvas) { + fileDownloadCanvasAsImage(canvas, `${props.title}.png`) + } + + }, [props.title, getCertificateCanvas]) + + const handlePrint: () => Promise + = useCertificatePrint(props.certificateElRef, props.title ?? '') + + useLayoutEffect(() => { + const el: HTMLElement = wrapElRef.current + if (props.fullScreenCertLayout !== true || !el) { + return + } + + hideSiblings(el) + hideSiblings(el.parentElement as HTMLElement) + el.classList.add(styles['full-screen-cert']) + }) + + return ( + <> + + + {props.isReady && ( +
+
+ {!props.fullScreenCertLayout && ( +
+ } + onClick={handleBackBtnClick} + /> +
+ )} +
+
+ {props.children} +
+
+ {!props.fullScreenCertLayout && ( +
+ } + onClick={handlePrint} + /> + } + onClick={handleDownload} + /> + {props.actions} + + + +
+ )} +
+ {props.afterContent} +
+ )} + + ) +} + +export default CertificatePageLayout diff --git a/src-ts/tools/learn/learn-lib/certificate-page-layout/index.ts b/src-ts/tools/learn/learn-lib/certificate-page-layout/index.ts new file mode 100644 index 000000000..85fa4eaa8 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/certificate-page-layout/index.ts @@ -0,0 +1 @@ +export { default as CertificatePageLayout, type CertificatePageLayoutStyle } from './CertificatePageLayout' diff --git a/src-ts/tools/learn/learn-lib/completion-time-range/CompletionTimeRange.module.scss b/src-ts/tools/learn/learn-lib/completion-time-range/CompletionTimeRange.module.scss new file mode 100644 index 000000000..3b8cad5ba --- /dev/null +++ b/src-ts/tools/learn/learn-lib/completion-time-range/CompletionTimeRange.module.scss @@ -0,0 +1,4 @@ +.wrap { + display: flex; + align-items: center; +} diff --git a/src-ts/tools/learn/learn-lib/completion-time-range/CompletionTimeRange.tsx b/src-ts/tools/learn/learn-lib/completion-time-range/CompletionTimeRange.tsx new file mode 100644 index 000000000..78b3b3181 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/completion-time-range/CompletionTimeRange.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react' + +import { TCACertificationCompletionTimeRange } from '../data-providers' + +import styles from './CompletionTimeRange.module.scss' + +interface CompletionTimeRangeProps { + range: TCACertificationCompletionTimeRange +} + +const CompletionTimeRange: FC = (props: CompletionTimeRangeProps) => ( +
+ + {props.range.lowRangeValue} + + + - + + + {props.range.highRangeValue} + +   + + {props.range.units} + +
+) + +export default CompletionTimeRange diff --git a/src-ts/tools/learn/learn-lib/completion-time-range/index.ts b/src-ts/tools/learn/learn-lib/completion-time-range/index.ts new file mode 100644 index 000000000..9e8e43e87 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/completion-time-range/index.ts @@ -0,0 +1,2 @@ +export { default as CompletionTimeRange } from './CompletionTimeRange' +export * from './use-hours-estimate-to-range' diff --git a/src-ts/tools/learn/learn-lib/completion-time-range/use-hours-estimate-to-range.tsx b/src-ts/tools/learn/learn-lib/completion-time-range/use-hours-estimate-to-range.tsx new file mode 100644 index 000000000..c04a6e36a --- /dev/null +++ b/src-ts/tools/learn/learn-lib/completion-time-range/use-hours-estimate-to-range.tsx @@ -0,0 +1,17 @@ +import { useMemo } from 'react' + +import { TCACertificationCompletionTimeRange } from '../data-providers' + +type useHoursEstimateToRangeFn = (hoursEstimate: number) => TCACertificationCompletionTimeRange + +export const useHoursEstimateToRange: useHoursEstimateToRangeFn += (hoursEstimate: number): TCACertificationCompletionTimeRange => { + + const completionTimeRange: TCACertificationCompletionTimeRange = useMemo(() => ({ + highRangeValue: hoursEstimate, + lowRangeValue: Math.ceil(hoursEstimate * (1 / 3)), + units: 'hours', + }), [hoursEstimate]) + + return completionTimeRange +} diff --git a/src-ts/tools/learn/learn-lib/course-badge/CourseBadge.tsx b/src-ts/tools/learn/learn-lib/course-badge/CourseBadge.tsx index 0905fb177..c830319da 100644 --- a/src-ts/tools/learn/learn-lib/course-badge/CourseBadge.tsx +++ b/src-ts/tools/learn/learn-lib/course-badge/CourseBadge.tsx @@ -1,13 +1,15 @@ import { FC, SVGProps } from 'react' +import classNames from 'classnames' -import { LearnCertificateTrackType } from '../all-certifications-provider' +import { TCACertificateType } from '../data-providers' import { getBadge, getBadgeImg } from './badges.functions' import styles from './CourseBadge.module.scss' interface CourseBadgeProps { asImg?: boolean - type: LearnCertificateTrackType + className?: string + type: TCACertificateType } const CourseBadge: FC = (props: CourseBadgeProps) => { @@ -21,7 +23,7 @@ const CourseBadge: FC = (props: CourseBadgeProps) => { const Badge: FC> = getBadge(props.type) return ( -
+
) diff --git a/src-ts/tools/learn/learn-lib/course-outline/CourseOutline.tsx b/src-ts/tools/learn/learn-lib/course-outline/CourseOutline.tsx index 1913fce14..5e2e21f22 100644 --- a/src-ts/tools/learn/learn-lib/course-outline/CourseOutline.tsx +++ b/src-ts/tools/learn/learn-lib/course-outline/CourseOutline.tsx @@ -14,6 +14,7 @@ import { CollapsibleItem } from './collapsible-item' import styles from './CourseOutline.module.scss' interface CourseOutlineProps { + certification: string course?: LearnCourse currentStep?: string onItemNavigate: (item: LearnLesson) => void @@ -25,11 +26,23 @@ const CourseOutline: FC = (props: CourseOutlineProps) => { const lessonPath: (course: LearnCourse, module: LearnModule, lesson: LearnLesson) => string = useCallback((course: LearnCourse, module: LearnModule, lesson: LearnLesson) => getLessonPathFromModule( - course.provider, - course.certification, + course.resourceProvider.name, + props.certification, module.key, lesson.dashedName, - ), []) + ), [props.certification]) + + function getItemKeyFn(module: LearnModule): (l: LearnLesson) => string { + return function getItemKey(lesson: LearnLesson): string { + return `${module.dashedName}/${lesson.dashedName}` + } + } + + function getItemPathFn(module: LearnModule): (l: LearnLesson) => string { + return function getItemPath(lesson: LearnLesson): string { + return (props.course ? lessonPath(props.course, module, lesson) : '') + } + } return (
@@ -41,18 +54,19 @@ const CourseOutline: FC = (props: CourseOutlineProps) => { {props.course.modules.map(module => ( `${module.meta.dashedName}/${it.dashedName}`} + itemId={getItemKeyFn(module)} items={module.lessons} key={module.key} lessonsCount={module.lessons.length} - path={(it: any) => (props.course ? lessonPath(props.course, module, it) : '')} - progress={props.progress?.modules} - shortDescription={module.meta.introCopy} - title={module.meta.name} + path={getItemPathFn(module)} + progress={props.progress?.moduleProgresses} + shortDescription={module.introCopy} + title={module.name} onItemClick={props.onItemNavigate} - isAssessment={module.meta.isAssessment} + isAssessment={module.isAssessment} /> ))}
diff --git a/src-ts/tools/learn/learn-lib/course-outline/collapsible-item/CollapsibleItem.tsx b/src-ts/tools/learn/learn-lib/course-outline/collapsible-item/CollapsibleItem.tsx index c7cef8ccf..cb8adb249 100644 --- a/src-ts/tools/learn/learn-lib/course-outline/collapsible-item/CollapsibleItem.tsx +++ b/src-ts/tools/learn/learn-lib/course-outline/collapsible-item/CollapsibleItem.tsx @@ -1,4 +1,13 @@ -import { Dispatch, FC, ReactNode, SetStateAction, useCallback, useMemo, useState } from 'react' +import { + Dispatch, + FC, + ReactNode, + SetStateAction, + useCallback, + useEffect, + useMemo, + useState, +} from 'react' import { Link } from 'react-router-dom' import classNames from 'classnames' @@ -22,20 +31,22 @@ interface CollapsibleListItem { interface CollapsibleItemProps { active?: string - duration: LearnModule['meta']['estimatedCompletionTime'] + duration: LearnModule['estimatedCompletionTimeValue'] + durationUnits: LearnModule['estimatedCompletionTimeUnits'] isAssessment: boolean - itemId?: (item: CollapsibleListItem) => string + itemId?: (item: any) => string items: Array lessonsCount: number moduleKey: string onItemClick: (item: any) => void path?: (item: any) => string - progress?: LearnUserCertificationProgress['modules'] + progress?: LearnUserCertificationProgress['moduleProgresses'] shortDescription: Array title: string } const CollapsibleItem: FC = (props: CollapsibleItemProps) => { + const hasActiveItem: boolean = props.items.some(item => props.itemId?.(item) === props.active) const [isOpen, setIsOpen]: [ boolean, Dispatch> @@ -49,7 +60,7 @@ const CollapsibleItem: FC = (props: CollapsibleItemProps) props.progress?.find(m => m.module === props.moduleKey) ), [props.progress, props.moduleKey]) - const isPartial: boolean = useMemo(() => !!progress && !!progress.completedLessons.length, [progress]) + const isPartial: boolean = useMemo(() => !!progress && !!progress.completedLessons?.length, [progress]) const isItemCompleted: (itemId: string) => boolean = (itemId: string) => ( progress?.moduleStatus === LearnModuleStatus.completed @@ -78,11 +89,15 @@ const CollapsibleItem: FC = (props: CollapsibleItemProps) const label: ReactNode = stepLabel(item, isActive, stepCount, !stepCount ? item.title : undefined) const key: string = props.itemId?.(item) ?? item.title + function handleClick(): void { + props.onItemClick(item) + } + return (
  • props.onItemClick(item)} + onClick={handleClick} > {props.path ? ( @@ -93,6 +108,20 @@ const CollapsibleItem: FC = (props: CollapsibleItemProps) ) } + /** + * Automatically open the parent module of the active lesson + */ + useEffect(() => { + if (!hasActiveItem || isOpen) { + return + } + + setIsOpen(hasActiveItem) + // only trigger this on `hasActiveItem` change, + // we don't want to trigger it on `isOpen` change - that will force the parent of the active item to always be open + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hasActiveItem]) + return (
    @@ -118,12 +147,12 @@ const CollapsibleItem: FC = (props: CollapsibleItemProps) {' '} Lessons - {props.duration.value !== 0 && ( + {!!props.duration && ( - {props.duration.value} + {props.duration} {' '} - {props.duration.units} + {props.durationUnits} )}
    diff --git a/src-ts/tools/learn/learn-lib/course-title/CourseTitle.tsx b/src-ts/tools/learn/learn-lib/course-title/CourseTitle.tsx index 6c0e8763c..864509e3e 100644 --- a/src-ts/tools/learn/learn-lib/course-title/CourseTitle.tsx +++ b/src-ts/tools/learn/learn-lib/course-title/CourseTitle.tsx @@ -1,14 +1,15 @@ import { FC, ReactNode } from 'react' import classNames from 'classnames' -import { LearnCertificateTrackType } from '../all-certifications-provider' +import { LearnCertificateTrackType, TCACertificationProviderBase } from '../data-providers' import { CourseBadge } from '../course-badge' +import { ProvidersLogoList } from '../providers-logo-list' import styles from './CourseTitle.module.scss' interface CourseTitleProps { children?: ReactNode - credits?: string + provider?: string size?: 'md'|'lg'|'xl' title: string trackType?: LearnCertificateTrackType @@ -38,11 +39,12 @@ const CourseTitle: FC = (props: CourseTitleProps) => { {props.children}
    - {props.credits && ( + {props.provider && ( - by - {' '} - {props.credits} + )} diff --git a/src-ts/tools/learn/learn-lib/curriculum-summary/CurriculumSummary.tsx b/src-ts/tools/learn/learn-lib/curriculum-summary/CurriculumSummary.tsx index d1c55a8eb..6168d3754 100644 --- a/src-ts/tools/learn/learn-lib/curriculum-summary/CurriculumSummary.tsx +++ b/src-ts/tools/learn/learn-lib/curriculum-summary/CurriculumSummary.tsx @@ -1,17 +1,18 @@ import { FC } from 'react' import { IconOutline } from '../../../../lib' -import { LearnCourse } from '../courses-provider' +import { LearnCourse } from '../data-providers' import styles from './CurriculumSummary.module.scss' interface CurriculumSummaryProps { - completionHours?: LearnCourse['estimatedCompletionTime'] + completionTimeValue?: LearnCourse['estimatedCompletionTimeValue'] + completionTimeUnits?: LearnCourse['estimatedCompletionTimeUnits'] moduleCount?: number } const CurriculumSummary: FC = (props: CurriculumSummaryProps) => { - const hasTimeEstimate: boolean = props.completionHours?.value !== 0 + const hasTimeEstimate: boolean = props.completionTimeValue !== 0 return (
    @@ -34,10 +35,10 @@ const CurriculumSummary: FC = (props: CurriculumSummaryP

    - {hasTimeEstimate ? props.completionHours?.value : (<> )} + {hasTimeEstimate ? props.completionTimeValue : (<> )}

    - {hasTimeEstimate ? (props.completionHours?.units ?? 'Hours') : 'Times vary'} + {hasTimeEstimate ? props.completionTimeUnits : 'Times vary'}
  • diff --git a/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/index.ts b/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/index.ts index 02ae1d59d..ee8f5ce11 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/index.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/index.ts @@ -2,3 +2,4 @@ export * from './all-certifications-provider-data.model' export * from './all-certifications.provider' export * from './learn-certificate-track-type' export * from './learn-certification.model' +export * from './learn-certification-level-type' diff --git a/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/learn-certificate-track-type.ts b/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/learn-certificate-track-type.ts index 89fe2b1fa..6b6cba418 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/learn-certificate-track-type.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/learn-certificate-track-type.ts @@ -1 +1 @@ -export type LearnCertificateTrackType = 'QA'|'DEV'|'DATASCIENCE'|'DESIGN'|'INTERVIEW'|'SECURITY' +export type LearnCertificateTrackType = 'QA' | 'DEV' | 'DATASCIENCE' | 'DESIGN' | 'DATABASE' | 'INTERVIEW' | 'SECURITY' diff --git a/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/learn-certification-level-type.ts b/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/learn-certification-level-type.ts new file mode 100644 index 000000000..d64e9e64f --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/learn-certification-level-type.ts @@ -0,0 +1 @@ +export type CertificationLearnLevel = 'Beginner' | 'Intermediate' | 'Expert' | 'All Levels' diff --git a/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/learn-certification.model.ts b/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/learn-certification.model.ts index f592b7c85..0c51a5e2f 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/learn-certification.model.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/all-certifications-provider/learn-certification.model.ts @@ -1,18 +1,25 @@ import { LearnModelBase } from '../../functions' +import { LearnCourse } from '../courses-provider' +import { ResourceProvider } from '../resource-provider-provider' +import { TCACertificationCategory } from '../tca-certifications-provider' -import { LearnCertificateTrackType } from './learn-certificate-track-type' +import { CertificationLearnLevel } from './learn-certification-level-type' export interface LearnCertification extends LearnModelBase { - category: string + certificationCategory: TCACertificationCategory certification: string certType: 'certification' | 'course-completion' completionHours: number + course: LearnCourse + description: string + fccId: string id: string key: string + learnerLevel: CertificationLearnLevel + moduleCount?: string | number providerCrertificationId: string - providerName: string publishedAt?: Date + resourceProvider: ResourceProvider state: 'active' | 'coming-soon' title: string - trackType: LearnCertificateTrackType } diff --git a/src-ts/tools/learn/learn-lib/data-providers/courses-provider/courses.provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/courses-provider/courses.provider.tsx index af4a5e52d..9ef5ce471 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/courses-provider/courses.provider.tsx +++ b/src-ts/tools/learn/learn-lib/data-providers/courses-provider/courses.provider.tsx @@ -24,8 +24,13 @@ export function useGetCourses( const { data, error }: SWRResponse> = useSWR(url, swrCacheConfig) + const course: LearnCourse | undefined = get(data, [0]) + + // sort modules by order property + course?.modules.sort((mA, mB) => mA.order - mB.order) + return { - course: get(data, [0]), + course, loading: !data && !error, ready: !!data || !!error, } diff --git a/src-ts/tools/learn/learn-lib/data-providers/courses-provider/learn-course.model.ts b/src-ts/tools/learn/learn-lib/data-providers/courses-provider/learn-course.model.ts index aae27afb8..48307e353 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/courses-provider/learn-course.model.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/courses-provider/learn-course.model.ts @@ -1,21 +1,24 @@ import { LearnModelBase } from '../../functions' +import { CertificationLearnLevel, LearnCertification } from '../all-certifications-provider' import { LearnModule } from '../lesson-provider' +import { ResourceProvider } from '../resource-provider-provider' export interface LearnCourse extends LearnModelBase { - certification: string certificationId: string completionSuggestions: Array - estimatedCompletionTime: { - units: string - value: number - } + estimatedCompletionTimeValue: number + estimatedCompletionTimeUnits: string + fccCourseUuid: string + freeCodeCampCertification: LearnCertification id: string introCopy: Array key: string keyPoints: Array + learnerLevel: CertificationLearnLevel moduleCount: number modules: Array note: string - provider: string + resourceProvider: ResourceProvider + skills: Array title: string } 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 ae66884e9..0d0093e10 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/index.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/index.ts @@ -4,3 +4,4 @@ export * from './lesson-provider' export * from './resource-provider-provider' export * from './user-certifications-provider' export * from './user-completed-certifications-provider' +export * from './tca-certifications-provider' diff --git a/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/index.ts b/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/index.ts index 9ca0da561..efb6fd8e7 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/index.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/index.ts @@ -1,6 +1,5 @@ export * from './learn-lesson-meta.model' export * from './learn-lesson.model' -export * from './learn-module-meta.model' export * from './learn-module.model' export * from './lesson-provider-data.model' export * from './lesson.provider' diff --git a/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-lesson-meta.model.ts b/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-lesson-meta.model.ts index 9a0aad0e3..0159e66c1 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-lesson-meta.model.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-lesson-meta.model.ts @@ -1,4 +1,5 @@ export interface LearnLessonMeta { + id: string course: { certification: string certificationId: string diff --git a/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-lesson.model.ts b/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-lesson.model.ts index 96b979736..5ffcbe1b4 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-lesson.model.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-lesson.model.ts @@ -1,5 +1,6 @@ export interface LearnLesson { dashedName: string id: string + isAssessment: true title: string } diff --git a/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-module-meta.model.ts b/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-module-meta.model.ts deleted file mode 100644 index bb2a59385..000000000 --- a/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-module-meta.model.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface LearnModuleMeta { - dashedName: string - estimatedCompletionTime: { - units: string - value: number - } - introCopy: Array - isAssessment: boolean - name: string -} diff --git a/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-module.model.ts b/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-module.model.ts index 45d0cbc88..e93d4d328 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-module.model.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/learn-module.model.ts @@ -1,8 +1,15 @@ +import { LearnModelBase } from '../../functions' + import { LearnLesson } from './learn-lesson.model' -import { LearnModuleMeta } from './learn-module-meta.model' -export interface LearnModule { +export interface LearnModule extends LearnModelBase { + dashedName: string + estimatedCompletionTimeValue: number + estimatedCompletionTimeUnits: string + introCopy: Array + isAssessment: boolean key: string lessons: Array - meta: LearnModuleMeta + name: string + order: number } diff --git a/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/lesson.provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/lesson.provider.tsx index ee1b20128..8bc5873ce 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/lesson.provider.tsx +++ b/src-ts/tools/learn/learn-lib/data-providers/lesson-provider/lesson.provider.tsx @@ -1,3 +1,5 @@ +import { get } from 'lodash' + import { CoursesProviderData, useGetCourses } from '../courses-provider' import { LearnLesson } from './learn-lesson.model' @@ -29,15 +31,15 @@ export function useGetLesson( lesson: !lessonData ? undefined : { ...lessonData, course: { - certification: courseData?.certification ?? '', + certification: get(courseData, 'freeCodeCampCertification.certification', ''), certificationId: courseData?.certificationId ?? '', id: courseData?.id ?? '', title: courseData?.title ?? '', }, lessonUrl, module: { - dashedName: moduleData?.meta.dashedName ?? '', - title: moduleData?.meta.name ?? '', + dashedName: moduleData?.dashedName ?? '', + title: moduleData?.name ?? '', }, }, loading, diff --git a/src-ts/tools/learn/learn-lib/data-providers/resource-provider-provider/resource-provider.model.ts b/src-ts/tools/learn/learn-lib/data-providers/resource-provider-provider/resource-provider.model.ts index df6ca66b2..8cf5c7b24 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/resource-provider-provider/resource-provider.model.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/resource-provider-provider/resource-provider.model.ts @@ -2,6 +2,7 @@ import { LearnModelBase } from '../../functions' export interface ResourceProvider extends LearnModelBase { attributionStatement: string + description: string id: string name: string url: string diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/index.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/index.ts new file mode 100644 index 000000000..8c079cb77 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/index.ts @@ -0,0 +1,19 @@ +export * from './tca-certificate-status-type' +export * from './tca-certificate-type' +export * from './tca-certificate-level-type' +export * from './tca-certification.model' +export * from './tca-provider-type' +export * from './tca-certification-provider.model-base' +export * from './tca-certification-provider.model' +export * from './tca-certification-resource.model' +export * from './tca-certification-resourceable.type' +export * from './tca-certification-category.model' +export * from './tca-certification-completion-time-range.model' + +export * from './tca-all-certifications' +export * from './tca-all-certifications-progress' +export * from './tca-certification' +export * from './tca-certification-progress' +export * from './tca-certification-enrollment-base.model' +export * from './tca-certification-validation' +export * from './tca-certification-enrollment' diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications-progress/index.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications-progress/index.ts new file mode 100644 index 000000000..a5ceccc39 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications-progress/index.ts @@ -0,0 +1,2 @@ +export * from './tca-all-certifications-progress-data.model' +export * from './tca-all-certifications-progress.provider' diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications-progress/tca-all-certifications-progress-data.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications-progress/tca-all-certifications-progress-data.model.ts new file mode 100644 index 000000000..f89fb7fc6 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications-progress/tca-all-certifications-progress-data.model.ts @@ -0,0 +1,8 @@ +import { TCACertificationProgress } from '../tca-certification-progress' + +export interface TCACertificationsProgressProviderData { + progresses: TCACertificationProgress[] + error: boolean + loading: boolean + ready: boolean +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications-progress/tca-all-certifications-progress.provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications-progress/tca-all-certifications-progress.provider.tsx new file mode 100644 index 000000000..33ff9ebc7 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications-progress/tca-all-certifications-progress.provider.tsx @@ -0,0 +1,35 @@ +import useSWR, { SWRConfiguration, SWRResponse } from 'swr' + +import { learnUrlGet } from '../../../functions' +import { useSwrCache } from '../../../learn-swr' + +import { TCACertificationsProgressProviderData } from './tca-all-certifications-progress-data.model' + +interface TCACertificationsProgressProviderOptions { + enabled?: boolean +} + +export function useGetAllTCACertificationsProgress( + userId: string, + options?: TCACertificationsProgressProviderOptions, +): TCACertificationsProgressProviderData { + + const url: string = learnUrlGet( + 'topcoder-certifications', + userId, + 'progresses', + ) + const swrCacheConfig: SWRConfiguration = useSwrCache(url) + + const { data, error }: SWRResponse = useSWR(url, { + ...swrCacheConfig, + isPaused: () => options?.enabled === false, + }) + + return { + error: !!error, + loading: !data, + progresses: data, + ready: !!data, + } +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications/index.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications/index.ts new file mode 100644 index 000000000..780ffc6e8 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications/index.ts @@ -0,0 +1,2 @@ +export * from './tca-all-certifications-data.model' +export * from './tca-all-certifications.provider' diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications/tca-all-certifications-data.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications/tca-all-certifications-data.model.ts new file mode 100644 index 000000000..ba0578f75 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications/tca-all-certifications-data.model.ts @@ -0,0 +1,8 @@ +import { TCACertification } from '../tca-certification.model' + +export interface TCACertificationsProviderData { + certifications: Array + error: boolean + loading: boolean + ready: boolean +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications/tca-all-certifications.provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications/tca-all-certifications.provider.tsx new file mode 100644 index 000000000..b79cf33dd --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-all-certifications/tca-all-certifications.provider.tsx @@ -0,0 +1,32 @@ +import useSWR, { SWRConfiguration, SWRResponse } from 'swr' + +import { learnUrlGet } from '../../../functions' +import { useSwrCache } from '../../../learn-swr' + +import { TCACertificationsProviderData } from './tca-all-certifications-data.model' + +interface TCACertificationsAllProviderOptions { + enabled?: boolean +} + +export function useGetAllTCACertifications( + options?: TCACertificationsAllProviderOptions, +): TCACertificationsProviderData { + + const url: string = learnUrlGet( + 'topcoder-certifications', + ) + const swrCacheConfig: SWRConfiguration = useSwrCache(url) + + const { data, error }: SWRResponse = useSWR(url, { + ...swrCacheConfig, + isPaused: () => options?.enabled === false, + }) + + return { + certifications: data ?? [], + error: !!error, + loading: !data, + ready: !!data, + } +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certificate-level-type.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certificate-level-type.ts new file mode 100644 index 000000000..098237cfa --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certificate-level-type.ts @@ -0,0 +1 @@ +export type TCACertificationLearnLevel = 'Beginner' | 'Intermediate' | 'Expert' | 'All Levels' diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certificate-status-type.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certificate-status-type.ts new file mode 100644 index 000000000..de5fb10cd --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certificate-status-type.ts @@ -0,0 +1 @@ +export type TCACertificationStatus = 'active' | 'inactive' | 'coming_soon' | 'deprecated' diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certificate-type.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certificate-type.ts new file mode 100644 index 000000000..66846af6d --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certificate-type.ts @@ -0,0 +1 @@ +export type TCACertificateType = 'QA' | 'DEV' | 'DATASCIENCE' | 'DESIGN' | 'DATABASE' | 'INTERVIEW' | 'SECURITY' 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..da71f59b6 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-category.model.ts @@ -0,0 +1,9 @@ +import { LearnModelBase } from '../../functions' + +import { TCACertificateType } from './tca-certificate-type' + +export interface TCACertificationCategory extends LearnModelBase { + id: number + category: string + track: TCACertificateType +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-completion-time-range.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-completion-time-range.model.ts new file mode 100644 index 000000000..623445659 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-completion-time-range.model.ts @@ -0,0 +1,5 @@ +export interface TCACertificationCompletionTimeRange { + highRangeValue: number + lowRangeValue: number + units: 'hours' +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-enrollment-base.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-enrollment-base.model.ts new file mode 100644 index 000000000..f9fe92ea7 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-enrollment-base.model.ts @@ -0,0 +1,15 @@ +import { TCACertification } from './tca-certification.model' + +export interface TCACertificationEnrollmentBase { + id: number + topcoderCertificationId: number + topcoderCertification?: TCACertification + userId: string + userHandle: string + userName: string + status: 'enrolled' + completedAt: null | Date | string + completionUuid: undefined | null | string + createdAt: Date + updatedAt: Date +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-enrollment/index.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-enrollment/index.ts new file mode 100644 index 000000000..28a7f28b5 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-enrollment/index.ts @@ -0,0 +1 @@ +export * from './tca-enrollment-provider' diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-enrollment/tca-enrollment-provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-enrollment/tca-enrollment-provider.tsx new file mode 100644 index 000000000..c8f2a611c --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-enrollment/tca-enrollment-provider.tsx @@ -0,0 +1,35 @@ +import useSWR, { SWRConfiguration, SWRResponse } from 'swr' + +import { learnUrlGet } from '../../../functions' +import { useSwrCache } from '../../../learn-swr' +import { TCACertificationEnrollmentBase } from '../tca-certification-enrollment-base.model' + +export interface TCACertificationEnrollmentProviderData { + enrollment: TCACertificationEnrollmentBase + error: boolean + loading: boolean + ready: boolean +} + +export function useTCACertificationEnrollment( + id: string, // note id | completionUuid both are supported by the API +): TCACertificationEnrollmentProviderData { + + const url: string = learnUrlGet( + 'certification-enrollment', + id, + ) + + const swrCacheConfig: SWRConfiguration = useSwrCache(url) + + const { data, error }: SWRResponse = useSWR(url, { + ...swrCacheConfig, + }) + + return { + enrollment: data, + error: !!error, + loading: !data, + ready: !!data, + } +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/index.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/index.ts new file mode 100644 index 000000000..fc639a5a7 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/index.ts @@ -0,0 +1,4 @@ +export * from './tca-certification-progress-data.model' +export * from './tca-certification-progress.provider' +export * from './tca-certification-progress.model' +export * from './tca-fcc-certification-progress.model' diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/tca-certification-progress-data.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/tca-certification-progress-data.model.ts new file mode 100644 index 000000000..4ac63cae6 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/tca-certification-progress-data.model.ts @@ -0,0 +1,10 @@ +import { TCACertificationProgress } from './tca-certification-progress.model' + +export interface TCACertificationProgressProviderData { + progress: TCACertificationProgress | undefined + error: boolean + loading: boolean + ready: boolean + refetch: () => void, + setCertificateProgress: (progess: TCACertificationProgress) => void, +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/tca-certification-progress.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/tca-certification-progress.model.ts new file mode 100644 index 000000000..fbb55d147 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/tca-certification-progress.model.ts @@ -0,0 +1,30 @@ +import { LearnModelBase } from '../../../functions' +import { UserCertificationProgressStatus } from '../../user-certifications-provider' +import { TCACertification } from '../tca-certification.model' + +import { TCAFccCertificationProgress } from './tca-fcc-certification-progress.model' + +export enum TCACertificationProgressStatus { + enrolled = 'enrolled', + completed = 'completed', +} + +export interface TCACertificationProgress extends LearnModelBase { + id: number + completionUuid: string + coursesCount: number + topcoderCertification: TCACertification + topcoderCertificationId: number + status: TCACertificationProgressStatus + completedAt: null | Date + certificationProgress: number + resourceProgresses: [{ + resourceProgressId: string + resourceProgressType: 'FccCertificationProgress' + fccCertificationProgress: TCAFccCertificationProgress + status: UserCertificationProgressStatus, + }] + userHandle: string + userId: number + userName: string +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/tca-certification-progress.provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/tca-certification-progress.provider.tsx new file mode 100644 index 000000000..894220202 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/tca-certification-progress.provider.tsx @@ -0,0 +1,92 @@ +import useSWR, { SWRConfiguration, SWRResponse } from 'swr' + +import { learnUrlGet, learnXhrGetAsync, learnXhrPutAsync } from '../../../functions' +import { useSwrCache } from '../../../learn-swr' + +import { TCACertificationProgressProviderData } from './tca-certification-progress-data.model' +import { TCACertificationProgress, TCACertificationProgressStatus } from './tca-certification-progress.model' + +interface TCACertificationProgressProviderOptions { + enabled?: boolean +} + +export function useGetTCACertificationProgress( + userId: string, + certification: string, + options?: TCACertificationProgressProviderOptions, +): TCACertificationProgressProviderData { + + const url: string = learnUrlGet( + 'topcoder-certifications', + certification, + userId, + 'progress', + ) + const swrCacheConfig: SWRConfiguration = useSwrCache(url) + + const { data, error, isValidating, mutate }: SWRResponse = useSWR(url, { + ...swrCacheConfig, + fetcher: (resUrl: string) => ( + learnXhrGetAsync(resUrl) + .then((progress: TCACertificationProgress) => ({ progress })) + ), + isPaused: () => options?.enabled === false, + }) + + return { + error: !!error, + loading: isValidating, + progress: data?.progress, + ready: !!data, + refetch: () => mutate(), + setCertificateProgress: progress => mutate([progress]), + } +} + +interface TCACertificationCompletedResponse { + topcoderCertificationStatus: { + status: TCACertificationProgressStatus + certification: string + } +} + +export interface TCACertificationCheckCompleted { + error: boolean + loading: boolean + certification: string | undefined + ready: boolean +} + +/** + * Checks if TCA certification has been completed + * @returns boolean + */ +export function useTCACertificationCheckCompleted( + resourceProgressType: string, + resourceProgressId: string | number, + options?: TCACertificationProgressProviderOptions, +): TCACertificationCheckCompleted { + + const url: string = learnUrlGet( + 'certification-enrollment-progresses', + resourceProgressType, + `${resourceProgressId}`, + ) + + const { data, error }: SWRResponse + = useSWR(url, { + fetcher: () => learnXhrPutAsync<{}, TCACertificationCompletedResponse>(url, {}), + isPaused: () => options?.enabled === false, + }) + + return { + certification: ( + data?.topcoderCertificationStatus?.status === 'completed' ? ( + data?.topcoderCertificationStatus.certification + ) : undefined + ), + error: !!error, + loading: !data, + ready: !!data, + } +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/tca-fcc-certification-progress.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/tca-fcc-certification-progress.model.ts new file mode 100644 index 000000000..03b697526 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-progress/tca-fcc-certification-progress.model.ts @@ -0,0 +1,9 @@ +import { LearnCertificateTrackType } from '../../all-certifications-provider' +import { LearnUserCertificationProgress } from '../../user-certifications-provider' + +export interface TCAFccCertificationProgress extends LearnUserCertificationProgress { + certificationTitle: string + certificationTrackType: LearnCertificateTrackType + fccCertificationId: string + certType: 'certification' +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-provider.model-base.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-provider.model-base.ts new file mode 100644 index 000000000..2ec892d4c --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-provider.model-base.ts @@ -0,0 +1,8 @@ +import { TCAProviderType } from './tca-provider-type' + +export interface TCACertificationProviderBase { + id: number + name: TCAProviderType + description: string + url: string +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-provider.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-provider.model.ts new file mode 100644 index 000000000..edea0cc79 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-provider.model.ts @@ -0,0 +1,7 @@ +import { TCACertificationProviderBase } from './tca-certification-provider.model-base' +import { TCACertificationResource } from './tca-certification-resource.model' + +export interface TCACertificationProvider extends TCACertificationProviderBase { + attributionStatement: string + CertificationResource: TCACertificationResource +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-resource.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-resource.model.ts new file mode 100644 index 000000000..3033216e0 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-resource.model.ts @@ -0,0 +1,22 @@ +import { LearnCertification } from '../all-certifications-provider' + +import { TCACertificationLearnLevel } from './tca-certificate-level-type' +import { TCACertificationResourceable } from './tca-certification-resourceable.type' + +export interface TCACertificationResource { + id: number + topcoderCertificationId: number + resourceProviderId: number + resourceableId: number + resourceableType: TCACertificationResourceable + displayOrder: number + completionOrder: number + resourceDescription: string + resourceTitle: string + createdAt: Date + updatedAt: Date + freeCodeCampCertification: LearnCertification & { + fccId: string + learnerLevel: TCACertificationLearnLevel + } +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-resourceable.type.tsx b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-resourceable.type.tsx new file mode 100644 index 000000000..3b887c08e --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-resourceable.type.tsx @@ -0,0 +1 @@ +export type TCACertificationResourceable = 'FreeCodeCampCertification' diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-validation/index.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-validation/index.ts new file mode 100644 index 000000000..52dfedb1c --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-validation/index.ts @@ -0,0 +1 @@ +export * from './tca-validation-provider' diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-validation/tca-validation-provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-validation/tca-validation-provider.tsx new file mode 100644 index 000000000..c983582a1 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification-validation/tca-validation-provider.tsx @@ -0,0 +1,41 @@ +import useSWR, { SWRConfiguration, SWRResponse } from 'swr' + +import { learnUrlGet } from '../../../functions' +import { TCACertificationEnrollmentBase } from '../tca-certification-enrollment-base.model' +import { TCACertification } from '../tca-certification.model' +import { useSwrCache } from '../../../learn-swr' + +export interface TCACertificationValidationData { + certification: TCACertification | undefined + enrollment: TCACertificationEnrollmentBase | undefined + error: boolean + loading: boolean + ready: boolean +} + +export function useValidateTCACertification( + dashedName: string, + userHandle: string, +): TCACertificationValidationData { + + const url: string = learnUrlGet( + 'topcoder-certifications', + dashedName, + userHandle, + 'validate', + ) + + const swrCacheConfig: SWRConfiguration = useSwrCache(url) + + const { data, error }: SWRResponse = useSWR(url, { + ...swrCacheConfig, + }) + + return { + certification: data?.certification, + enrollment: data?.enrollment, + error: !!error, + loading: !(data || error), + ready: !!(data || error), + } +} 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 new file mode 100644 index 000000000..c38129728 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification.model.ts @@ -0,0 +1,32 @@ +import { TCACertificationLearnLevel } from './tca-certificate-level-type' +import { TCACertificationStatus } from './tca-certificate-status-type' +import { TCACertificationCategory } from './tca-certification-category.model' +import { TCACertificationCompletionTimeRange } from './tca-certification-completion-time-range.model' +import { TCACertificationProvider } from './tca-certification-provider.model' +import { TCACertificationProviderBase } from './tca-certification-provider.model-base' +import { TCACertificationResource } from './tca-certification-resource.model' + +export interface TCACertification { + certificationCategory: TCACertificationCategory + certificationCategoryId: string + certificationResources: Array + completionTimeRange: TCACertificationCompletionTimeRange + coursesCount: number + createdAt: Date + dashedName: string + description: string + id: number + introText: string + learnerLevel: TCACertificationLearnLevel + learnedOutcomes: string[] + learningOutcomes: string[] + prerequisites: string[] + providers: Array + resourceProviders: Array + sequentialCourses: boolean + skills: string[] + status: TCACertificationStatus + stripeProductId?: string + title: string + updatedAt: Date +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification/index.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification/index.ts new file mode 100644 index 000000000..35aeac33d --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification/index.ts @@ -0,0 +1,2 @@ +export * from './tca-certification-data.model' +export * from './tca-certification.provider' diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification/tca-certification-data.model.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification/tca-certification-data.model.ts new file mode 100644 index 000000000..4591cf78c --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification/tca-certification-data.model.ts @@ -0,0 +1,8 @@ +import { TCACertification } from '../tca-certification.model' + +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-certification/tca-certification.provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification/tca-certification.provider.tsx new file mode 100644 index 000000000..5547e0809 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-certification/tca-certification.provider.tsx @@ -0,0 +1,49 @@ +import useSWR, { SWRConfiguration, SWRResponse } from 'swr' + +import { learnUrlGet, learnXhrPostAsync } from '../../../functions' +import { useSwrCache } from '../../../learn-swr' +import { TCACertificationProgress } from '../tca-certification-progress' + +import { TCACertificationProviderData } from './tca-certification-data.model' + +interface TCACertificationProviderOptions { + enabled?: boolean +} + +export function useGetTCACertification( + certification: string, + options?: TCACertificationProviderOptions, +): TCACertificationProviderData { + + const url: string = learnUrlGet( + 'topcoder-certifications', + certification, + ) + const swrCacheConfig: SWRConfiguration = useSwrCache(url) + + const { data, error }: SWRResponse = useSWR(url, { + ...swrCacheConfig, + isPaused: () => options?.enabled === false, + }) + + return { + certification: data, + error: !!error, + loading: !data, + ready: !!data, + } +} + +export function enrollTCACertificationAsync( + userId: string, + certificationId: string, +): Promise { + + const url: string = learnUrlGet( + 'topcoder-certifications', + userId, + certificationId, + 'enroll', + ) + return learnXhrPostAsync<{}, TCACertificationProgress>(url, {}, {}) +} diff --git a/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-provider-type.ts b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-provider-type.ts new file mode 100644 index 000000000..583b5b09e --- /dev/null +++ b/src-ts/tools/learn/learn-lib/data-providers/tca-certifications-provider/tca-provider-type.ts @@ -0,0 +1 @@ +export type TCAProviderType = 'freeCodeCamp' | 'Topcoder' diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications-functions/learn-user-certification-progress.model.ts b/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications-functions/learn-user-certification-progress.model.ts index 5c39b423c..dada14bfd 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications-functions/learn-user-certification-progress.model.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications-functions/learn-user-certification-progress.model.ts @@ -1,13 +1,19 @@ import { LearnModelBase } from '../../../functions' +import { LearnCertificateTrackType, LearnCertification } from '../../all-certifications-provider' +import { ResourceProvider } from '../../resource-provider-provider' import { LearnModuleProgress } from './learn-module-progress.model' import { UserCertificationProgressStatus } from './user-certification-progress-status.enum' export interface LearnUserCertificationProgress extends LearnModelBase { academicHonestyPolicyAcceptedAt?: number, + certProgressDynamoUuid: string certification: string certificationId: string certificationProgressPercentage: number + certificationImageUrl: string + certificationTrackType: LearnCertificateTrackType + certificationTitle: string certType: 'certification' completedDate?: string courseId: string @@ -15,8 +21,12 @@ export interface LearnUserCertificationProgress extends LearnModelBase { courseProgressPercentage: number currentLesson?: string id: string - modules: Array - provider: string + fccCertificationId: string + fccCourseId: string + freeCodeCampCertification: LearnCertification + moduleProgresses: Array + resourceProvider: ResourceProvider startDate: string status: UserCertificationProgressStatus + userId: string } diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications-functions/user-certification-progress.store.ts b/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications-functions/user-certification-progress.store.ts index 92c6754cb..ddd57420c 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications-functions/user-certification-progress.store.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications-functions/user-certification-progress.store.ts @@ -7,7 +7,7 @@ import { LearnUserCertificationProgress } from './learn-user-certification-progr import { UserCertificationUpdateProgressActions } from './user-certification-update-progress-actions.enum' const certProgressPath: string = 'certification-progresses' -const certProgressShortcutPath = 'shortcut-fcc-course-completion' +const certProgressShortcutPath: string = 'shortcut-fcc-course-completion' export function completeCourse( certificationProgressId: string, diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications-provider-data.model.ts b/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications-provider-data.model.ts index ad1d645d5..70a638456 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications-provider-data.model.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/user-certifications-provider/user-certifications-provider-data.model.ts @@ -1,6 +1,7 @@ import { LearnUserCertificationProgress } from './user-certifications-functions' export interface UserCertificationsProviderData { + progresses: ReadonlyArray completed: ReadonlyArray inProgress: ReadonlyArray(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 @@ -57,6 +62,7 @@ export function useGetUserCertifications( completed, inProgress, loading: !!userId && loading, + progresses: data ?? [], // ready when: // profile context was initialized and diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/index.ts b/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/index.ts index ddf12865e..1f989cc8a 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/index.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/index.ts @@ -1,3 +1,2 @@ export * from './user-completed-certifications-provider-data.model' export * from './user-completed-certifications.provider' -export * from './user-completed-certification.model' diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/user-completed-certification.model.ts b/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/user-completed-certification.model.ts deleted file mode 100644 index e74b44573..000000000 --- a/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/user-completed-certification.model.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { LearnModelBase } from '../../functions' -import { LearnCertificateTrackType } from '../all-certifications-provider' -import { UserCertificationProgressStatus } from '../user-certifications-provider' - -export interface LearnUserCompletedCertification extends LearnModelBase { - certification: string, - certificationId: string, - certificationTitle: string, - certificationTrackType: LearnCertificateTrackType, - certType: 'certification' | 'course-completion', - completedDate: string, - provider: string, - providerUrl: string - status: UserCertificationProgressStatus, -} diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/user-completed-certifications-provider-data.model.ts b/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/user-completed-certifications-provider-data.model.ts index f5791b508..b88950349 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/user-completed-certifications-provider-data.model.ts +++ b/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/user-completed-certifications-provider-data.model.ts @@ -1,7 +1,7 @@ -import { LearnUserCompletedCertification } from './user-completed-certifications-functions' +import { LearnUserCertificationProgress } from '../user-certifications-provider' export interface UserCompletedCertificationsProviderData { - certifications: ReadonlyArray + certifications: ReadonlyArray loading: boolean ready: boolean } diff --git a/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/user-completed-certifications.provider.tsx b/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/user-completed-certifications.provider.tsx index 5e56ea7cf..58ab7f44c 100644 --- a/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/user-completed-certifications.provider.tsx +++ b/src-ts/tools/learn/learn-lib/data-providers/user-completed-certifications-provider/user-completed-certifications.provider.tsx @@ -1,8 +1,10 @@ +import { get } from 'lodash' import useSWR, { SWRResponse } from 'swr' import { learnUrlGet } from '../../functions' +import { TCACertificationEnrollmentBase } from '../tca-certifications-provider' +import { LearnUserCertificationProgress } from '../user-certifications-provider' -import { LearnUserCompletedCertification } from './user-completed-certification.model' import { UserCompletedCertificationsProviderData } from './user-completed-certifications-provider-data.model' export function useGetUserCompletedCertifications( @@ -13,14 +15,19 @@ export function useGetUserCompletedCertifications( const url: string = learnUrlGet('completed-certifications', `${userId}`) - const { data, error }: SWRResponse> = useSWR(url) + const { data, error }: SWRResponse<{ + enrollments: ReadonlyArray, + courses: ReadonlyArray, + }> = useSWR(url) - let certifications: ReadonlyArray = data ?? [] + let certifications: ReadonlyArray = [] - if (provider || certification) { - certifications = certifications - .filter(c => (!provider || c.provider === provider) - && (!certification || c.certification === certification)) + if (provider && certification) { + certifications = (data?.courses ?? []) + .filter(c => ( + get(c, 'resourceProvider.name') === provider + && c.certification === certification + )) } return { diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/CertBadgeIcon.module.scss b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/CertBadgeIcon.module.scss new file mode 100644 index 000000000..b075267c7 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/CertBadgeIcon.module.scss @@ -0,0 +1,28 @@ +@import '../../../../../lib/styles/includes'; + +.iconse { + path:global(.star) { + opacity: 0.23; + } + + &:global(.beginner), + &:global(.intermediate), + &:global(.expert) { + path:global(.star-1) { + opacity: 1; + } + } + + &:global(.intermediate), + &:global(.expert) { + path:global(.star-2) { + opacity: 1; + } + } + + &:global(.expert) { + path:global(.star-3) { + opacity: 1; + } + } +} diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/CertBadgeIcon.tsx b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/CertBadgeIcon.tsx new file mode 100644 index 000000000..e155a2781 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/CertBadgeIcon.tsx @@ -0,0 +1,22 @@ +import { FC, SVGProps } from 'react' +import classNames from 'classnames' + +import { TCACertificateType, TCACertificationLearnLevel } from '../../data-providers' + +import { getCertBadgeIcon } from './badges' +import styles from './CertBadgeIcon.module.scss' + +interface CertBadgeIconProps { + type: TCACertificateType + level: TCACertificationLearnLevel +} + +const CertificateBadgeIcon: FC = (props: CertBadgeIconProps) => { + const Icon: FC> = getCertBadgeIcon(props.type) + + return ( + + ) +} + +export default CertificateBadgeIcon diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/database.svg b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/database.svg new file mode 100644 index 000000000..681ed6218 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/database.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/datascience.svg b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/datascience.svg new file mode 100644 index 000000000..37404eeae --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/datascience.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/design.svg b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/design.svg new file mode 100644 index 000000000..4dcd322ae --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/design.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/dev.svg b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/dev.svg new file mode 100644 index 000000000..c65d591ef --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/dev.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/index.tsx b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/index.tsx new file mode 100644 index 000000000..be8d79ff3 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/index.tsx @@ -0,0 +1,25 @@ +import { FC, SVGProps } from 'react' + +import { TCACertificateType } from '../../../data-providers' + +import { ReactComponent as DatabaseCertBadgeSvg } from './database.svg' +import { ReactComponent as DatascienceCertBadgeSvg } from './datascience.svg' +import { ReactComponent as DesignCertBadgeSvg } from './design.svg' +import { ReactComponent as DevCertBadgeSvg } from './dev.svg' +import { ReactComponent as InterviewCertBadgeSvg } from './interview.svg' +import { ReactComponent as QaCertBadgeSvg } from './qa.svg' +import { ReactComponent as SecurityCertBadgeSvg } from './security.svg' + +export const certBadgeIconMap: {[key in TCACertificateType]: FC} = { + DATABASE: DatabaseCertBadgeSvg, + DATASCIENCE: DatascienceCertBadgeSvg, + DESIGN: DesignCertBadgeSvg, + DEV: DevCertBadgeSvg, + INTERVIEW: InterviewCertBadgeSvg, + QA: QaCertBadgeSvg, + SECURITY: SecurityCertBadgeSvg, +} + +export function getCertBadgeIcon(certificateTrackType: TCACertificateType): FC> { + return certBadgeIconMap[certificateTrackType] +} diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/interview.svg b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/interview.svg new file mode 100644 index 000000000..9b3b725f3 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/interview.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/qa.svg b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/qa.svg new file mode 100644 index 000000000..89dad408b --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/qa.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/security.svg b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/security.svg new file mode 100644 index 000000000..8910ffc06 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/badges/security.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/index.ts b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/index.ts new file mode 100644 index 000000000..cedf6970e --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/cert-badge-icon/index.ts @@ -0,0 +1 @@ +export { default as CertificateBadgeIcon } from './CertBadgeIcon' diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/index.ts b/src-ts/tools/learn/learn-lib/dynamic-icons/index.ts new file mode 100644 index 000000000..4bc8258d1 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/index.ts @@ -0,0 +1,2 @@ +export * from './cert-badge-icon' +export * from './learn-level-icon' diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/learn-level-icon/LearnLevelIcon.module.scss b/src-ts/tools/learn/learn-lib/dynamic-icons/learn-level-icon/LearnLevelIcon.module.scss new file mode 100644 index 000000000..781c034ec --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/learn-level-icon/LearnLevelIcon.module.scss @@ -0,0 +1,28 @@ +@import '../../../../../lib/styles/includes'; + +.icon { + &:global(.beginner), + &:global(.intermediate), + &:global(.expert) { + > path:first-child { + fill: $blue-140; + stroke-width: 0; + } + } + + &:global(.intermediate), + &:global(.expert) { + > path:nth-child(2) { + fill: $blue-140; + stroke-width: 0; + } + } + + &:global(.expert) { + > path:nth-child(3) { + fill: $blue-140; + stroke-width: 0; + } + } + +} diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/learn-level-icon/LearnLevelIcon.tsx b/src-ts/tools/learn/learn-lib/dynamic-icons/learn-level-icon/LearnLevelIcon.tsx new file mode 100644 index 000000000..efd9f6928 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/learn-level-icon/LearnLevelIcon.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react' +import classNames from 'classnames' + +import { TCACertificationLearnLevel } from '../../data-providers' + +import { ReactComponent as Icon } from './icon-level-3.svg' +import styles from './LearnLevelIcon.module.scss' + +interface LearnLevelIconProps { + level: TCACertificationLearnLevel +} + +const LearnLevelIcon: FC = (props: LearnLevelIconProps) => ( + +) + +export default LearnLevelIcon diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/learn-level-icon/icon-level-3.svg b/src-ts/tools/learn/learn-lib/dynamic-icons/learn-level-icon/icon-level-3.svg new file mode 100644 index 000000000..a7b6c8fd5 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/learn-level-icon/icon-level-3.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src-ts/tools/learn/learn-lib/dynamic-icons/learn-level-icon/index.ts b/src-ts/tools/learn/learn-lib/dynamic-icons/learn-level-icon/index.ts new file mode 100644 index 000000000..00d9b6266 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/dynamic-icons/learn-level-icon/index.ts @@ -0,0 +1 @@ +export { default as LearnLevelIcon } from './LearnLevelIcon' diff --git a/src-ts/tools/learn/learn-lib/functions/clean-fcc-certif-title.ts b/src-ts/tools/learn/learn-lib/functions/clean-fcc-certif-title.ts new file mode 100644 index 000000000..e3f39676c --- /dev/null +++ b/src-ts/tools/learn/learn-lib/functions/clean-fcc-certif-title.ts @@ -0,0 +1,6 @@ +/** + * Removes the "certification" at the end of an FCC certificaiton's title + */ +export const clearFCCCertificationTitle: (title: string) => string = (title: string): string => ( + title.replace(/\s*Certification\s*$/i, '') +) diff --git a/src-ts/tools/learn/learn-lib/functions/hide-siblings.ts b/src-ts/tools/learn/learn-lib/functions/hide-siblings.ts new file mode 100644 index 000000000..ffcfccf1d --- /dev/null +++ b/src-ts/tools/learn/learn-lib/functions/hide-siblings.ts @@ -0,0 +1,7 @@ +export function hideSiblings(el: HTMLElement): void { + [].forEach.call(el.parentElement?.children ?? [], (c: HTMLElement) => { + if (c !== el) { + Object.assign(c.style, { display: 'none' }) + } + }) +} diff --git a/src-ts/tools/learn/learn-lib/functions/index.ts b/src-ts/tools/learn/learn-lib/functions/index.ts index a91806c85..4b1cda188 100644 --- a/src-ts/tools/learn/learn-lib/functions/index.ts +++ b/src-ts/tools/learn/learn-lib/functions/index.ts @@ -5,3 +5,5 @@ export { postAsync as learnXhrPostAsync, putAsync as learnXhrPutAsync, } from './learn-xhr.functions' +export { hideSiblings } from './hide-siblings' +export * from './clean-fcc-certif-title' diff --git a/src-ts/tools/learn/learn-lib/index.ts b/src-ts/tools/learn/learn-lib/index.ts index 6a21a12f8..53bdd29e4 100755 --- a/src-ts/tools/learn/learn-lib/index.ts +++ b/src-ts/tools/learn/learn-lib/index.ts @@ -1,11 +1,26 @@ +export * from './action-button' +export * from './certificate-not-found-content' +export * from './certificate-page-layout' export * from './collapsible-pane' +export * from './completion-time-range' export * from './course-badge' export * from './course-outline' export * from './course-title' export * from './curriculum-summary' export * from './data-providers' +export * from './dynamic-icons' +export * from './functions' export * from './learn-breadcrumb-provider' export * from './learn-swr' -export * from './my-course-card' +export * from './providers-logo-list' +export * from './skill-tags' +export * from './skill' +export * from './sticky-sidebar' export * from './svgs' +export * from './tca-certificate-preview' +export * from './tca-certification-completed-modal' +export * from './tca-certification-progress-box' +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/my-course-card/completed/Completed.module.scss b/src-ts/tools/learn/learn-lib/my-course-card/completed/Completed.module.scss deleted file mode 100644 index f651dc58e..000000000 --- a/src-ts/tools/learn/learn-lib/my-course-card/completed/Completed.module.scss +++ /dev/null @@ -1,48 +0,0 @@ -@use '../../../../../lib/styles/typography'; -@import '../../../../../lib/styles/includes'; - -.wrap { - background: $tc-white; - padding: $space-xxl; - border-radius: $space-sm; - @include ltelg { - padding: $space-lg; - } -} - -.line { - display: flex; - align-items: flex-start; - flex-wrap: wrap; - - margin-bottom: $space-lg; -} - -.completed-status { - @extend .body-main-bold; - color: $blue-140; - white-space: nowrap; - - &:before { - content: ""; - display: inline-block; - margin: 2px $space-sm -2px; - width: 1px; - height: 16px; - background: $black-60; - @include ltemd { - content: none; - } - } -} - -.buttons-wrap { - display: flex; - align-items: center; - gap: $space-sm; - flex-wrap: wrap; - - @include ltemd { - gap: $space-lg; - } -} diff --git a/src-ts/tools/learn/learn-lib/my-course-card/completed/Completed.tsx b/src-ts/tools/learn/learn-lib/my-course-card/completed/Completed.tsx deleted file mode 100644 index 6813bc99d..000000000 --- a/src-ts/tools/learn/learn-lib/my-course-card/completed/Completed.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { FC } from 'react' -import classNames from 'classnames' - -import { Button, textFormatDateLocaleShortString } from '../../../../../lib' -import { CourseTitle, LearnCertification } from '../..' -import { getCertificatePath, getCoursePath } from '../../../learn.routes' - -import styles from './Completed.module.scss' - -interface CompletedProps { - certification?: LearnCertification - completed: string -} - -const Completed: FC = (props: CompletedProps) => { - - if (!props.certification) { - return <> - } - - return ( -
    -
    - -
    - Completed - {' '} - {textFormatDateLocaleShortString(new Date(props.completed))} -
    -
    -
    -
    -
    -
    - ) -} - -export default Completed diff --git a/src-ts/tools/learn/learn-lib/my-course-card/completed/index.tsx b/src-ts/tools/learn/learn-lib/my-course-card/completed/index.tsx deleted file mode 100644 index 9473bcf78..000000000 --- a/src-ts/tools/learn/learn-lib/my-course-card/completed/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default as Completed } from './Completed' diff --git a/src-ts/tools/learn/learn-lib/my-course-card/in-progress/InProgress.module.scss b/src-ts/tools/learn/learn-lib/my-course-card/in-progress/InProgress.module.scss deleted file mode 100644 index 152d7f6fb..000000000 --- a/src-ts/tools/learn/learn-lib/my-course-card/in-progress/InProgress.module.scss +++ /dev/null @@ -1,122 +0,0 @@ -@use '../../../../../lib/styles/typography'; -@import '../../../../../lib/styles/includes'; - -.wrap { - background: $tc-white; - padding: $space-lg $space-xxl; - border-radius: $space-sm; - - display: flex; - - @include ltelg { - &, &.large { - padding: $space-lg; - } - } - - @include ltemd { - &.large { - flex-direction: column; - } - } -} - -.inner { - flex: 1 1 0; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: $space-lg; -} - -.line { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - @include ltelg { - flex-wrap: wrap; - gap: $space-lg; - } -} - -.status { - @extend .body-main-bold; - white-space: nowrap; - color: $blue-140; - - &:before { - content: ""; - display: inline-block; - margin: 2px $space-sm -2px; - width: 1px; - height: 16px; - background: $black-60; - @include ltemd { - content: none; - } - } -} - -.summary { - display: flex; - align-items: center; - margin-top: $space-lg; - width: 100%; - justify-content: space-between; - gap: $space-lg; - @include ltemd { - flex-direction: column; - align-items: flex-start; - margin-top: 0; - } -} - -.details { - display: flex; - align-items: flex-start; - flex: 1 1 0; - &:before { - content: ""; - display: block; - height: 100%; - width: 1px; - background: $black-10; - margin: 0 calc($space-mx + $space-sm); - } - - &-inner { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: $space-sm; - - p { - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; - } - - :global(.button) { - margin-top: $space-sm; - } - } - - @include ltemd { - flex-direction: column; - &:before { - margin: $space-xxl 0; - width: 100%; - height: 1px; - } - } -} - -.started-date { - font-weight: bold; - - @include ltemd { - order: -1; - } -} diff --git a/src-ts/tools/learn/learn-lib/my-course-card/in-progress/InProgress.tsx b/src-ts/tools/learn/learn-lib/my-course-card/in-progress/InProgress.tsx deleted file mode 100644 index a31cb4ddd..000000000 --- a/src-ts/tools/learn/learn-lib/my-course-card/in-progress/InProgress.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { FC } from 'react' -import { NavigateFunction, useNavigate } from 'react-router-dom' -import classNames from 'classnames' - -import { - Button, - ProgressBar, - textFormatDateLocaleShortString, - textFormatGetSafeString, -} from '../../../../../lib' -import { - CoursesProviderData, - CourseTitle, - LearnCertification, - useGetCourses, -} from '../..' -import { getCoursePath, getLessonPathFromCurrentLesson } from '../../../learn.routes' -import { CurriculumSummary } from '../../curriculum-summary' - -import styles from './InProgress.module.scss' - -interface InProgressProps { - certification?: LearnCertification - completedPercentage: number - currentLesson?: string - startDate?: string - theme: 'detailed' | 'minimum' -} - -const InProgress: FC = (props: InProgressProps) => { - - const navigate: NavigateFunction = useNavigate() - const isDetailed: boolean = props.theme === 'detailed' - const isMinimum: boolean = props.theme === 'minimum' - - const certification: string = textFormatGetSafeString(props.certification?.certification) - const provider: string = textFormatGetSafeString(props.certification?.providerName) - const { course }: CoursesProviderData = useGetCourses(provider, certification) - - const resumeCourse: () => void = () => { - - if (!props.currentLesson) { - return - } - - const coursePath: string = getLessonPathFromCurrentLesson( - provider, - certification, - props.currentLesson, - ) - navigate(coursePath) - } - - return ( -
    -
    -
    - - {isDetailed && ( -
    In Progress
    - )} -
    - {isMinimum && ( -
    - - - {isMinimum && ( -
    -
    - )} -
    - {isDetailed && ( -
    -
    - {/* eslint-disable-next-line react/no-danger */} -


    ') ?? '' }} /> - {props.startDate && ( -

    - Started - {textFormatDateLocaleShortString(new Date(props.startDate))} -
    - )} -
    -
    - )} -
    - ) -} - -export default InProgress diff --git a/src-ts/tools/learn/learn-lib/my-course-card/in-progress/index.ts b/src-ts/tools/learn/learn-lib/my-course-card/in-progress/index.ts deleted file mode 100644 index 9cc92ff49..000000000 --- a/src-ts/tools/learn/learn-lib/my-course-card/in-progress/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as InProgress } from './InProgress' diff --git a/src-ts/tools/learn/learn-lib/my-course-card/index.ts b/src-ts/tools/learn/learn-lib/my-course-card/index.ts deleted file mode 100755 index 583e014bf..000000000 --- a/src-ts/tools/learn/learn-lib/my-course-card/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { InProgress as MyCourseInProgressCard } from './in-progress' -export { Completed as MyCourseCompletedCard } from './completed' diff --git a/src-ts/tools/learn/learn-lib/providers-logo-list/ProvidersLogoList.module.scss b/src-ts/tools/learn/learn-lib/providers-logo-list/ProvidersLogoList.module.scss new file mode 100644 index 000000000..2b67a134c --- /dev/null +++ b/src-ts/tools/learn/learn-lib/providers-logo-list/ProvidersLogoList.module.scss @@ -0,0 +1,29 @@ +@import '../../../../lib/styles/includes'; + +.wrap { + display: flex; + align-items: center; + gap: $space-sm; + text-transform: none; + + @include ltelg { + align-items: flex-start; + } + + .providersWrap { + display: flex; + align-items: center; + gap: $space-lg; + margin-top: -3px; + + @include ltelg { + flex-direction: column; + gap: 0; + } + + > svg { + height: 22px; + display: block; + } + } +} diff --git a/src-ts/tools/learn/learn-lib/providers-logo-list/ProvidersLogoList.tsx b/src-ts/tools/learn/learn-lib/providers-logo-list/ProvidersLogoList.tsx new file mode 100644 index 000000000..74dae064a --- /dev/null +++ b/src-ts/tools/learn/learn-lib/providers-logo-list/ProvidersLogoList.tsx @@ -0,0 +1,28 @@ +import { FC, Fragment } from 'react' +import classNames from 'classnames' + +import { TCACertification } from '..' + +import { getProviderLogo } from './providers-logo-map' +import styles from './ProvidersLogoList.module.scss' + +interface ProvidersLogoListProps { + label: string + className?: string + providers: TCACertification['providers'] +} + +const ProvidersLogoList: FC = (props: ProvidersLogoListProps) => ( +
    + + {props.label} + +
    + {props.providers.map(p => ( + {getProviderLogo(p.name)} + ))} +
    +
    +) + +export default ProvidersLogoList diff --git a/src-ts/tools/learn/learn-lib/providers-logo-list/index.ts b/src-ts/tools/learn/learn-lib/providers-logo-list/index.ts new file mode 100644 index 000000000..b42ab04d4 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/providers-logo-list/index.ts @@ -0,0 +1 @@ +export { default as ProvidersLogoList } from './ProvidersLogoList' diff --git a/src-ts/tools/learn/learn-lib/providers-logo-list/providers-logo-map.tsx b/src-ts/tools/learn/learn-lib/providers-logo-list/providers-logo-map.tsx new file mode 100644 index 000000000..2d3d4ad7c --- /dev/null +++ b/src-ts/tools/learn/learn-lib/providers-logo-list/providers-logo-map.tsx @@ -0,0 +1,13 @@ +import { ReactNode } from 'react' + +import { FccLogoBlackSvg, TcLogoSvg } from '../svgs' +import { TCAProviderType } from '../data-providers' + +export const providersLogoMap: {[key in TCAProviderType]: ReactNode} = { + freeCodeCamp: , + Topcoder: , +} + +export function getProviderLogo(provider: TCAProviderType): ReactNode { + return providersLogoMap[provider] +} diff --git a/src-ts/tools/learn/learn-lib/skill-tags/SkillTags.module.scss b/src-ts/tools/learn/learn-lib/skill-tags/SkillTags.module.scss new file mode 100644 index 000000000..182a711da --- /dev/null +++ b/src-ts/tools/learn/learn-lib/skill-tags/SkillTags.module.scss @@ -0,0 +1,26 @@ +@import '../../../../lib/styles/includes'; + +.skills { + display: flex; + flex-wrap: wrap; + margin-top: $space-sm; + + >div { + margin-right: $space-sm; + margin-bottom: $space-sm; + + &:last-child { + margin-right: 0; + } + } +} + +.infoText { + font-style: italic; + color: $black-60; + margin-right: $space-sm; + + &:last-child { + margin-right: 0; + } +} diff --git a/src-ts/tools/learn/learn-lib/skill-tags/SkillTags.tsx b/src-ts/tools/learn/learn-lib/skill-tags/SkillTags.tsx new file mode 100644 index 000000000..73bd9588a --- /dev/null +++ b/src-ts/tools/learn/learn-lib/skill-tags/SkillTags.tsx @@ -0,0 +1,36 @@ +import { FC } from 'react' +import classNames from 'classnames' + +import { Tooltip } from '../../../../lib' +import { SkillLabel } from '..' + +import styles from './SkillTags.module.scss' + +interface SkillTagsProps { + courseKey: string + expandCount?: number + theme?: 'white' | 'gray' | undefined + skills: Array | null | undefined +} + +const SkillTags: FC = (props: SkillTagsProps) => { + const expandCount: number = props.expandCount || 3 + const theme: 'white' | 'gray' = props.theme || 'white' + + return ( +
    + skills taught + {props.skills?.slice(0, expandCount) + .map((skill: string) => )} + {props.skills?.length > expandCount && ( + } + /> + )} +
    + ) +} + +export default SkillTags diff --git a/src-ts/tools/learn/learn-lib/skill-tags/index.ts b/src-ts/tools/learn/learn-lib/skill-tags/index.ts new file mode 100644 index 000000000..d3db5264c --- /dev/null +++ b/src-ts/tools/learn/learn-lib/skill-tags/index.ts @@ -0,0 +1 @@ +export { default as SkillTags } from './SkillTags' diff --git a/src-ts/tools/learn/learn-lib/skill/SkillLabel.module.scss b/src-ts/tools/learn/learn-lib/skill/SkillLabel.module.scss new file mode 100644 index 000000000..e59c83e8f --- /dev/null +++ b/src-ts/tools/learn/learn-lib/skill/SkillLabel.module.scss @@ -0,0 +1,17 @@ +@import '../../../../lib/styles/includes'; + +.wrap, +.wrapGray { + background-color: $tc-white; + padding: 0 $space-sm; + border-radius: 4px; + + span { + font-style: italic; + color: $black-80; + } +} + +.wrapGray { + background-color: $black-5; +} diff --git a/src-ts/tools/learn/learn-lib/skill/SkillLabel.tsx b/src-ts/tools/learn/learn-lib/skill/SkillLabel.tsx new file mode 100644 index 000000000..cd483257e --- /dev/null +++ b/src-ts/tools/learn/learn-lib/skill/SkillLabel.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react' + +import styles from './SkillLabel.module.scss' + +interface SkillLabelProps { + skill: string + theme: 'white' | 'gray' | undefined +} + +const SkillLabel: FC = (props: SkillLabelProps) => ( +
    + {props.skill} +
    +) + +export default SkillLabel diff --git a/src-ts/tools/learn/learn-lib/skill/index.ts b/src-ts/tools/learn/learn-lib/skill/index.ts new file mode 100644 index 000000000..ec7bf7ebd --- /dev/null +++ b/src-ts/tools/learn/learn-lib/skill/index.ts @@ -0,0 +1 @@ +export { default as SkillLabel } from './SkillLabel' diff --git a/src-ts/tools/learn/learn-lib/sticky-sidebar/StickySidebar.module.scss b/src-ts/tools/learn/learn-lib/sticky-sidebar/StickySidebar.module.scss new file mode 100644 index 000000000..ad6703ad5 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/sticky-sidebar/StickySidebar.module.scss @@ -0,0 +1,49 @@ +@import '../../../../lib/styles/includes'; + +.sticky-container { + @include ltelg { + @include contentWidth; + padding-top: $space-xxl; + } + + @include gtelg { + position: absolute; + top: 65px; + width: 445px; + padding: 40px 0; + bottom: 0; + } + + @media (min-width: $lg-min) and (max-width: 1150px){ + width: 40vw; + } + + @include ltexxl { + right: $space-xxxxl; + } + + @include xxl { + right: calc(50vw - calc($xxl-min/2 - $space-xxl)); + } + +} + +.wrap { + background: $tc-white; + border: $border-xs solid $black-20; + border-radius: $space-sm; + + width: 100%; + margin: 0 auto; + + padding: $space-xxl; + + position: sticky; + top: 40px; + + @include ltemd { + padding: $space-lg; + position: relative; + top: 0; + } +} diff --git a/src-ts/tools/learn/learn-lib/sticky-sidebar/StickySidebar.tsx b/src-ts/tools/learn/learn-lib/sticky-sidebar/StickySidebar.tsx new file mode 100644 index 000000000..eaaeb1434 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/sticky-sidebar/StickySidebar.tsx @@ -0,0 +1,19 @@ +import classNames from 'classnames' +import { FC, ReactNode } from 'react' + +import styles from './StickySidebar.module.scss' + +interface StickySidebarProps { + children: ReactNode + className?: string +} + +const StickySidebar: FC = (props: StickySidebarProps) => ( +
    +
    + {props.children} +
    +
    +) + +export default StickySidebar diff --git a/src-ts/tools/learn/learn-lib/sticky-sidebar/index.ts b/src-ts/tools/learn/learn-lib/sticky-sidebar/index.ts new file mode 100644 index 000000000..da9f7560f --- /dev/null +++ b/src-ts/tools/learn/learn-lib/sticky-sidebar/index.ts @@ -0,0 +1 @@ +export { default as StickySidebar } from './StickySidebar' diff --git a/src-ts/tools/learn/learn-lib/svgs/Doug-Hanson.svg b/src-ts/tools/learn/learn-lib/svgs/Doug-Hanson.svg new file mode 100644 index 000000000..7ff725100 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/svgs/Doug-Hanson.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/svgs/cert-icon.svg b/src-ts/tools/learn/learn-lib/svgs/cert-icon.svg new file mode 100644 index 000000000..1be452e4e --- /dev/null +++ b/src-ts/tools/learn/learn-lib/svgs/cert-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/svgs/course-icon.svg b/src-ts/tools/learn/learn-lib/svgs/course-icon.svg new file mode 100644 index 000000000..37ff1622f --- /dev/null +++ b/src-ts/tools/learn/learn-lib/svgs/course-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/svgs/crowd-icon.svg b/src-ts/tools/learn/learn-lib/svgs/crowd-icon.svg new file mode 100644 index 000000000..999301dde --- /dev/null +++ b/src-ts/tools/learn/learn-lib/svgs/crowd-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/svgs/icon-certif.svg b/src-ts/tools/learn/learn-lib/svgs/icon-certif.svg new file mode 100644 index 000000000..cfd540f6f --- /dev/null +++ b/src-ts/tools/learn/learn-lib/svgs/icon-certif.svg @@ -0,0 +1,3 @@ + + + diff --git a/src-ts/tools/learn/learn-lib/svgs/index.ts b/src-ts/tools/learn/learn-lib/svgs/index.ts index 224066d9b..ee0ce16b8 100644 --- a/src-ts/tools/learn/learn-lib/svgs/index.ts +++ b/src-ts/tools/learn/learn-lib/svgs/index.ts @@ -1,3 +1,8 @@ -import { ReactComponent as LearningHat } from './learning-hat.svg' - -export { LearningHat } +export { ReactComponent as LearningHat } from './learning-hat.svg' +export { ReactComponent as CertIcon } from './cert-icon.svg' +export { ReactComponent as CourseIcon } from './course-icon.svg' +export { ReactComponent as CrowdIcon } from './crowd-icon.svg' +export { ReactComponent as TcLogoSvg } from './tc-logo.svg' +export { ReactComponent as FccLogoBlackSvg } from './vendor-fcc-logo-black.svg' +export { ReactComponent as IconCertifSvg } from './icon-certif.svg' +export { ReactComponent as DougSigSvg } from './Doug-Hanson.svg' diff --git a/src-ts/tools/learn/learn-lib/svgs/tc-logo.svg b/src-ts/tools/learn/learn-lib/svgs/tc-logo.svg new file mode 100644 index 000000000..27cbe639e --- /dev/null +++ b/src-ts/tools/learn/learn-lib/svgs/tc-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/svgs/vendor-fcc-logo-black.svg b/src-ts/tools/learn/learn-lib/svgs/vendor-fcc-logo-black.svg new file mode 100644 index 000000000..5f963d208 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/svgs/vendor-fcc-logo-black.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/tca-certificate-preview/TCACertificatePreview.module.scss b/src-ts/tools/learn/learn-lib/tca-certificate-preview/TCACertificatePreview.module.scss new file mode 100644 index 000000000..2a31ea57d --- /dev/null +++ b/src-ts/tools/learn/learn-lib/tca-certificate-preview/TCACertificatePreview.module.scss @@ -0,0 +1,44 @@ +@import '../../../../lib/styles/includes'; + +.wrap { + flex: 1 1 auto; + aspect-ratio: $tca-certif-aspect-ratio; + position: relative; + + body:global(.canvas-clone) & { + transform: none!important; + } +} + +.inner { + position: absolute; + top: 0; + left: 0; + width: 880px; + aspect-ratio: $tca-certif-aspect-ratio; + + > * { + position: absolute; + top: 0; + left: 0; + } +} + + +:global(.large-container) { + .wrap { + aspect-ratio: unset; + width: 100%; + height: 100%; + transform: none!important; + } + + .inner { + position: static; + width: 100%; + height: 100%; + > * { + position: relative; + } + } +} \ No newline at end of file diff --git a/src-ts/tools/learn/learn-lib/tca-certificate-preview/TCACertificatePreview.tsx b/src-ts/tools/learn/learn-lib/tca-certificate-preview/TCACertificatePreview.tsx new file mode 100644 index 000000000..03e953660 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/tca-certificate-preview/TCACertificatePreview.tsx @@ -0,0 +1,58 @@ +import { FC, MutableRefObject, ReactNode, useRef } from 'react' +import classNames from 'classnames' + +import { TCACertification, useCertificateScaling } from '..' + +import { TCACertificate } from './tca-certificate' +import styles from './TCACertificatePreview.module.scss' + +interface TCACertificatePreviewProps { + className?: string + certificateElRef?: MutableRefObject + certification: TCACertification + userName?: string + tcHandle?: string + completedDate?: string + completionUuid?: string + validateLink?: string +} + +const TCACertificatePreview: FC = (props: TCACertificatePreviewProps) => { + const certificateWrapRef: MutableRefObject = useRef() + + useCertificateScaling(certificateWrapRef, 880, 880) + + function getPlaceholder(): ReactNode { + return ( + + ) + } + + function getCompletedCertificate(): ReactNode { + return ( + + ) + } + + return ( +
    +
    + {props.userName && props.completedDate ? getCompletedCertificate() : getPlaceholder()} +
    +
    + ) +} + +export default TCACertificatePreview diff --git a/src-ts/tools/learn/learn-lib/tca-certificate-preview/index.ts b/src-ts/tools/learn/learn-lib/tca-certificate-preview/index.ts new file mode 100644 index 000000000..c650b141b --- /dev/null +++ b/src-ts/tools/learn/learn-lib/tca-certificate-preview/index.ts @@ -0,0 +1,2 @@ +export { default as TCACertificatePreview } from './TCACertificatePreview' +export * from './tca-certificate' diff --git a/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/TCACertificate.module.scss b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/TCACertificate.module.scss new file mode 100644 index 000000000..0ff114f4a --- /dev/null +++ b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/TCACertificate.module.scss @@ -0,0 +1,192 @@ +@import '../../../../../lib/styles/includes'; + +.wrap { + background-color: $tc-white; + height: 100%; + position: relative; + z-index: 1; +} + +.background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; +} + +// TODO: add cross track theme +.theme { + display: flex; + height: 100%; + color: $black-100; + background-repeat: no-repeat; + background-size: cover; + + :global(.large-container) & { + background-size: 100% 100%; + } + + &-dev { + .certTitle { + color: $tc-dev-track-color; + } + + :global(.grad) { + @include grad-text-color($tc-dev-grad); + } + } + + &-datascience { + .certTitle { + color: $tc-datascience-track-color; + } + + :global(.grad) { + @include grad-text-color($tc-datascience-grad); + } + } + + &-design { + .certTitle { + color: $tc-design-track-color; + } + + :global(.grad) { + @include grad-text-color($tc-design-grad); + } + } + + &-qa { + .certTitle { + color: $tc-qa-track-color; + } + + :global(.grad) { + @include grad-text-color($tc-qa-grad); + } + } + + .details { + width: 55%; + padding: calc($space-mx + $space-lg); + display: flex; + flex-direction: column; + flex: 1; + + .headerWrap { + display: flex; + justify-content: space-between; + align-items: flex-start; + + >svg { + width: 120px; + height: 120px; + } + + .logos { + display: flex; + + .logosDivider { + width: 2px; + height: 40px; + background-color: $tc-white; + opacity: 0.3; + margin: 0 $space-lg; + } + } + } + + .certWrap { + display: flex; + flex-direction: column; + margin: auto 0; + + .certOwner { + font-family: $font-barlow; + font-weight: $font-weight-semibold; + font-size: 34px; + line-height: 32px; + color: $black-100; + margin-bottom: $space-xl; + } + + .certText { + font-size: 13px; + margin-bottom: $space-sm; + } + + .certTitle { + font-family: $font-barlow-condensed; + font-weight: $font-weight-semibold; + font-size: 80px; + line-height: 72px; + text-transform: uppercase; + } + } + + .certInfo { + display: flex; + justify-content: space-between; + align-items: center; + + .certInfoLeft { + display: flex; + align-items: center; + + .qrCode { + margin-right: $space-lg; + opacity: 0.7; + border: 0.5px solid #D4D4D4; + border-radius: 2px; + background-color: $tc-white; + } + + .certInfoLeftData { + display: grid; + grid-template-columns: 88px 1fr; + + >span { + font-size: 9px; + line-height: 11px; + } + + .gridSeparator { + margin-bottom: $space-sm; + } + } + } + + .sigWrap { + display: flex; + flex-direction: column; + + .divider { + height: 2px; + background: $black-10; + border-radius: 1px; + margin: $space-xs 0; + } + + >span { + font-size: 9px; + line-height: 11px; + } + } + } + } +} + +:global(.large-container) { + .certWrap { + max-width: 800px; + } +} + +// print/download styles +:global(.canvas-clone) { + .certText { + margin-bottom: $space-xxxl !important; + } +} diff --git a/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/TCACertificate.tsx b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/TCACertificate.tsx new file mode 100644 index 000000000..93df04fd9 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/TCACertificate.tsx @@ -0,0 +1,115 @@ +import { FC, MutableRefObject } from 'react' +import { QRCodeSVG } from 'qrcode.react' +import classNames from 'classnames' +import moment from 'moment' + +import { TCAcademyLogoWhiteSvg, TCLogoSvg } from '../../../../../lib' +import { LearnConfig } from '../../../learn-config' +import { TCACertificateType, TCACertification } from '../../data-providers' +import { CertificateBadgeIcon } from '../../dynamic-icons' +import { DougSigSvg } from '../../svgs' + +import { CertificateBackground } from './certificate-background' +import styles from './TCACertificate.module.scss' + +interface TCACertificateProps { + certification: TCACertification + completionUuid?: null | string + completedDate?: string + displaySignature?: boolean + elRef?: MutableRefObject + tcHandle?: string + userName?: string + validateLink?: string +} + +const TCACertificate: FC = (props: TCACertificateProps) => { + // TODO: add cross track theme/type support + const certificateType: TCACertificateType = props.certification.certificationCategory?.track ?? 'DEV' + + const displaySignature: boolean = props.displaySignature ?? true + + const completedDate: string = moment(props.completedDate || new Date()) + .format('MMM D, YYYY') + + // TODO: revisit this when certs expirations are defined, now just +1 year + const expireDate: string = moment(props.completedDate || new Date()) + .add(1, 'year') + .format('MMM D, YYYY') + + const elementSelector: { [attr: string]: string } = { + [LearnConfig.CERT_ELEMENT_SELECTOR.attribute]: LearnConfig.CERT_ELEMENT_SELECTOR.value, + } + + return ( +
    +
    + +
    +
    + +
    + +
    + +
    +
    +
    +
    {props.userName || props.tcHandle || 'Your Name'}
    +

    + has successfully completed the certification requirements and has been awarded +

    +
    + {props.certification.title} +
    +
    + { + props.completedDate && props.validateLink && ( +
    +
    + +
    + Date of certification + {completedDate} + Valid through + + {expireDate} + + Serial Number + {props.completionUuid} + Validate at + {props.validateLink} +
    +
    + { + displaySignature && ( +
    + +
    + Doug Hanson + CEO, Topcoder +
    + ) + } +
    + ) + } +
    +
    +
    + ) +} + +export default TCACertificate diff --git a/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/certificate-background/CertificateBackground.module.scss b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/certificate-background/CertificateBackground.module.scss new file mode 100644 index 000000000..a8b7b0ecb --- /dev/null +++ b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/certificate-background/CertificateBackground.module.scss @@ -0,0 +1,28 @@ +@import '../../../../../../lib/styles/includes'; + +.bg { + path:global(#tca-cert-bg_grad-el) { + fill: url(#tca-cert-bg--default); + } + &:global(.dev) { + path:global(#tca-cert-bg_grad-el) { + fill: url(#tca-cert-bg--dev); + } + } + &:global(.datascience) { + path:global(#tca-cert-bg_grad-el) { + fill: url(#tca-cert-bg--datascience); + } + } + &:global(.design) { + path:global(#tca-cert-bg_grad-el) { + fill: url(#tca-cert-bg--design); + } + } + &:global(.qa) { + path:global(#tca-cert-bg_grad-el) { + fill: url(#tca-cert-bg--qa); + } + } + +} diff --git a/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/certificate-background/CertificateBackground.tsx b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/certificate-background/CertificateBackground.tsx new file mode 100644 index 000000000..a220af954 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/certificate-background/CertificateBackground.tsx @@ -0,0 +1,20 @@ +import { FC } from 'react' +import classNames from 'classnames' + +import { TCACertificateType } from '../../../data-providers' + +import { ReactComponent as BackgroundSvg } from './certificate-bg.svg' +import styles from './CertificateBackground.module.scss' + +interface CertificateBackgroundProps { + className?: string + certType: TCACertificateType +} + +const CertificateBackground: FC = (props: CertificateBackgroundProps) => ( + +) + +export default CertificateBackground diff --git a/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/certificate-background/certificate-bg.svg b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/certificate-background/certificate-bg.svg new file mode 100644 index 000000000..4c3fb3f41 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/certificate-background/certificate-bg.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/certificate-background/index.ts b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/certificate-background/index.ts new file mode 100644 index 000000000..e527ee9c4 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/certificate-background/index.ts @@ -0,0 +1 @@ +export { default as CertificateBackground } from './CertificateBackground' diff --git a/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/index.ts b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/index.ts new file mode 100644 index 000000000..a1c71cbf0 --- /dev/null +++ b/src-ts/tools/learn/learn-lib/tca-certificate-preview/tca-certificate/index.ts @@ -0,0 +1 @@ +export { default as TCACertificate } from './TCACertificate' diff --git a/src-ts/tools/learn/learn-lib/tca-certification-completed-modal/TCACertificationCompletedModal.module.scss b/src-ts/tools/learn/learn-lib/tca-certification-completed-modal/TCACertificationCompletedModal.module.scss new file mode 100644 index 000000000..ab48f247b --- /dev/null +++ b/src-ts/tools/learn/learn-lib/tca-certification-completed-modal/TCACertificationCompletedModal.module.scss @@ -0,0 +1,38 @@ +@import '../../../../lib/styles/includes'; + +.completedModal.completedModal { + color: $tc-white; + background: url(./bg.jpg) no-repeat center / cover, $tc-grad20; + max-width: 560px; + + @include ltemd { + background: url(./bg-mobile.jpg) 0 0 no-repeat, $tc-grad20; + background-size: cover; + } + + &:global(.react-responsive-modal-modal) { + padding: 0; + min-height: 224px; + + :global(.modal-body) { + padding: $space-xxl; + margin: 0; + + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-start; + gap: $space-xxl; + + @include ltemd { + justify-content: center; + } + } + } + + h2 { + text-transform: none; + font-size: 26px; + line-height: 32px; + } +} diff --git a/src-ts/tools/learn/learn-lib/tca-certification-completed-modal/TCACertificationCompletedModal.tsx b/src-ts/tools/learn/learn-lib/tca-certification-completed-modal/TCACertificationCompletedModal.tsx new file mode 100644 index 000000000..11f3d64fc --- /dev/null +++ b/src-ts/tools/learn/learn-lib/tca-certification-completed-modal/TCACertificationCompletedModal.tsx @@ -0,0 +1,63 @@ +import { Dispatch, FC, SetStateAction, useEffect } from 'react' + +import { BaseModal, BaseModalProps, Button, useSessionStorage } from '../../../../lib' +import { TCACertification } from '../data-providers' +import { getTCACertificateUrl } from '../../learn.routes' + +import styles from './TCACertificationCompletedModal.module.scss' + +interface TCACertificationCompletedModalProps extends BaseModalProps { + certification: TCACertification + isOpen: boolean +} + +const TCACertificationCompletedModal: FC += (props: TCACertificationCompletedModalProps) => { + + const storeKey: string = props.certification?.dashedName && `tca-cert-completed[${props.certification.dashedName}]` + + const [isOpen, setIsOpen]: [boolean, Dispatch>] + = useSessionStorage(storeKey, false) + + function handleClick(): void { + handleClose() + window.open(getTCACertificateUrl(props.certification.dashedName), '_blank') + } + + function handleClose(): void { + setIsOpen(false) + props.onClose() + } + + useEffect(() => { + if (!storeKey || sessionStorage.getItem(storeKey) !== null) { + return + } + + setIsOpen(props.isOpen) + }, [props.isOpen, setIsOpen, storeKey]) + + return ( + + <> +

    + You have successfully completed the + {' '} + {props.certification.title} + {' '} + Certification + ! +

    +
    - ) - - const renderCertificationsList: () => ReactNode = () => ( - hasCertifications ? certifications.map(certif => ( - - )) : renderPlaceholder() - ) - - return ( - -
    - {renderCertificationsList()} -
    -
    - ) -} - -export default CompletedTab diff --git a/src-ts/tools/learn/my-learning/completed-tab/index.ts b/src-ts/tools/learn/my-learning/completed-tab/index.ts deleted file mode 100644 index 954a7e44f..000000000 --- a/src-ts/tools/learn/my-learning/completed-tab/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as CompletedTab } from './CompletedTab' diff --git a/src-ts/tools/learn/my-learning/hero-card/HeroCard.module.scss b/src-ts/tools/learn/my-learning/hero-card/HeroCard.module.scss deleted file mode 100755 index 9cc12d4cc..000000000 --- a/src-ts/tools/learn/my-learning/hero-card/HeroCard.module.scss +++ /dev/null @@ -1,32 +0,0 @@ -@use '../../../../lib/styles/typography'; -@import '../../../../lib/styles/includes'; - -.wrap { - background: $black-5; - border-radius: $space-sm; - padding: $space-xxl; - color: $black-100; - - display: flex; - flex-direction: column; - gap: $space-sm; - width: 100%; - - :global(.button.link) { - white-space: initial; - } - @include ltelg { - padding: $space-lg; - } -} - -.line { - display: flex; - align-items: center; - gap: $space-sm; - justify-content: flex-end; - @include ltelg { - flex-direction: column; - align-items: flex-start; - } -} \ No newline at end of file diff --git a/src-ts/tools/learn/my-learning/hero-card/HeroCard.tsx b/src-ts/tools/learn/my-learning/hero-card/HeroCard.tsx deleted file mode 100755 index 67ea6cb7e..000000000 --- a/src-ts/tools/learn/my-learning/hero-card/HeroCard.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { FC } from 'react' - -import { EnvironmentConfig } from '../../../../config' -import { Button } from '../../../../lib' - -import styles from './HeroCard.module.scss' - -interface HeroCardProps { - userHandle?: string -} - -const HeroCard: FC = (props: HeroCardProps) => ( -
    -
    - Learning looks good on you. -
    -
    - Put your new skills to use. -
    -
    - Get that Gig! -
    - -
    -) - -export default HeroCard diff --git a/src-ts/tools/learn/my-learning/hero-card/index.ts b/src-ts/tools/learn/my-learning/hero-card/index.ts deleted file mode 100755 index 26b7f32b6..000000000 --- a/src-ts/tools/learn/my-learning/hero-card/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as HeroCard } from './HeroCard' diff --git a/src-ts/tools/learn/my-learning/in-progress-tab/InProgressTab.module.scss b/src-ts/tools/learn/my-learning/in-progress-tab/InProgressTab.module.scss deleted file mode 100644 index 97024c9d8..000000000 --- a/src-ts/tools/learn/my-learning/in-progress-tab/InProgressTab.module.scss +++ /dev/null @@ -1,26 +0,0 @@ -@import '../../../../lib/styles/includes'; - -.wrap { - gap: $space-lg; - display: flex; - flex-direction: column; - position: relative; - flex: 1; - - @include ltemd { - gap: $space-xxl; - } - - > :global(.course-card-wrap) { - background: $black-5; - } -} - -.placeholder-wrap { - flex: 1 1 auto; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: $space-xxl; -} diff --git a/src-ts/tools/learn/my-learning/in-progress-tab/InProgressTab.tsx b/src-ts/tools/learn/my-learning/in-progress-tab/InProgressTab.tsx deleted file mode 100644 index 8c9595906..000000000 --- a/src-ts/tools/learn/my-learning/in-progress-tab/InProgressTab.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { FC, ReactNode } from 'react' - -import { Button } from '../../../../lib' -import { LearnCertification, MyCourseInProgressCard, UserCertificationInProgress } from '../../learn-lib' -import { LEARN_PATHS } from '../../learn.routes' -import { sortOptions } from '../my-learning-sort-options' -import { MyTabsViews } from '../my-tabs-navbar' -import { TabContentLayout } from '../tab-content-layout' -import { useSortAndFilter, UseSortAndFilterValue } from '../use-sort-and-filter' - -import styles from './InProgressTab.module.scss' - -interface InProgressTabProps { - allCertificates: ReadonlyArray - certificatesById: {[key: string]: LearnCertification} - certifications: ReadonlyArray -} - -const InProgressTab: FC = (props: InProgressTabProps) => { - - const { - handleCategoryChange, - certifications, - handleSortChange, - }: UseSortAndFilterValue = useSortAndFilter( - props.allCertificates, - props.certifications, - ) - - const disableFilters: boolean = props.certifications.length < 1 - const hasCertifications: boolean = certifications.length >= 1 - - const renderPlaceholder: () => ReactNode = () => ( -
    -
    - Your In Progress courses will live here. Let’s go! -
    -
    - ) - - const renderCertificationsList: () => ReactNode = () => ( - hasCertifications ? certifications.map(certif => ( - - )) : renderPlaceholder() - ) - - return ( - -
    - {renderCertificationsList()} -
    -
    - ) -} - -export default InProgressTab diff --git a/src-ts/tools/learn/my-learning/in-progress-tab/index.ts b/src-ts/tools/learn/my-learning/in-progress-tab/index.ts deleted file mode 100644 index 93afb5922..000000000 --- a/src-ts/tools/learn/my-learning/in-progress-tab/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as InProgressTab } from './InProgressTab' diff --git a/src-ts/tools/learn/my-learning/my-learning-sort-options.ts b/src-ts/tools/learn/my-learning/my-learning-sort-options.ts deleted file mode 100644 index dd620a11d..000000000 --- a/src-ts/tools/learn/my-learning/my-learning-sort-options.ts +++ /dev/null @@ -1,26 +0,0 @@ -export interface SortOption { - label: string, - value: string, -} - -const COMPLETED_SORT_OPTIONS: ReadonlyArray = [ - { label: 'Completion date: new to old', value: '-completedDate' }, - { label: 'Completion date: old to new', value: 'completedDate' }, -] - -const IN_PROGRESS_SORT_OPTIONS: ReadonlyArray = [ - { label: 'Recent activity: new to old', value: '-updatedAt' }, - { label: 'Recent activity: old to new', value: 'updatedAt' }, - { label: 'Most progress', value: '-courseProgressPercentage' }, - { label: 'Least progress', value: 'courseProgressPercentage' }, - { label: 'Name', value: 'certification' }, - // {label: 'Length', field: (c: ), direction: 'asc'}, -] - -export const sortOptions: { - completed: ReadonlyArray, - inProgress: ReadonlyArray -} = { - completed: COMPLETED_SORT_OPTIONS, - inProgress: IN_PROGRESS_SORT_OPTIONS, -} diff --git a/src-ts/tools/learn/my-learning/my-tabs-navbar/MyTabsNavbar.module.scss b/src-ts/tools/learn/my-learning/my-tabs-navbar/MyTabsNavbar.module.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src-ts/tools/learn/my-learning/my-tabs-navbar/MyTabsNavbar.tsx b/src-ts/tools/learn/my-learning/my-tabs-navbar/MyTabsNavbar.tsx deleted file mode 100644 index 684190deb..000000000 --- a/src-ts/tools/learn/my-learning/my-tabs-navbar/MyTabsNavbar.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Dispatch, FC, ReactNode, SetStateAction, useEffect, useMemo, useState } from 'react' - -import { TabsNavbar, TabsNavItem } from '../../../../lib' - -import { getMyTabsNavbarTabs, MyTabsViews } from './tabs-navbar.config' - -interface MyTabsNavbarProps { - children: ReactNode - completed: number - inProgress: number - onTabChange: (activeTab: MyTabsViews) => void -} - -const MyTabsNavbar: FC = (props: MyTabsNavbarProps) => { - const [activeTab, setActiveTab]: [MyTabsViews, Dispatch>] - = useState(MyTabsViews.inProgress) - - const tabs: ReadonlyArray = useMemo(() => getMyTabsNavbarTabs( - props.completed, - props.inProgress, - ), [props.completed, props.inProgress]) - - const handleOnChange: (evTab: string) => void = (evTab: string) => { - const tab: MyTabsViews = evTab as MyTabsViews - setActiveTab(tab) - props.onTabChange(tab) - } - - useEffect(() => { - props.onTabChange(activeTab) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.onTabChange]) - - return ( - <> - - {props.children} - - ) -} - -export default MyTabsNavbar diff --git a/src-ts/tools/learn/my-learning/my-tabs-navbar/index.tsx b/src-ts/tools/learn/my-learning/my-tabs-navbar/index.tsx deleted file mode 100644 index 1e9b05c55..000000000 --- a/src-ts/tools/learn/my-learning/my-tabs-navbar/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { default as MyTabsNavbar } from './MyTabsNavbar' -export * from './tabs-navbar.config' diff --git a/src-ts/tools/learn/my-learning/my-tabs-navbar/tabs-navbar.config.ts b/src-ts/tools/learn/my-learning/my-tabs-navbar/tabs-navbar.config.ts deleted file mode 100644 index 4b17892a6..000000000 --- a/src-ts/tools/learn/my-learning/my-tabs-navbar/tabs-navbar.config.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TabsNavItem } from '../../../../lib' - -export enum MyTabsViews { - inProgress = 'Courses In Progress', - completed = 'Completed Courses', -} - -export const getMyTabsNavbarTabs: ( - completed: number, - inProgress: number -) => ReadonlyArray = ( - completed: number, - inProgress: number, -) => [ - { - badges: [ - { count: inProgress, type: 'info' }, - ], - id: MyTabsViews.inProgress, - title: MyTabsViews.inProgress, - }, - { - badges: [ - { count: completed, type: 'info' }, - ], - id: MyTabsViews.completed, - title: MyTabsViews.completed, - }, -] diff --git a/src-ts/tools/learn/my-learning/tab-content-layout/TabContentLayout.module.scss b/src-ts/tools/learn/my-learning/tab-content-layout/TabContentLayout.module.scss deleted file mode 100644 index 17a2d9eac..000000000 --- a/src-ts/tools/learn/my-learning/tab-content-layout/TabContentLayout.module.scss +++ /dev/null @@ -1,69 +0,0 @@ -@import '../../../../lib/styles/includes'; - -.wrap { - padding: $space-xxxxl 0; - gap: $space-lg; - display: flex; - flex-direction: column; - position: relative; - min-height: 320px; - - flex: 1 1 auto; - - &:empty { - display: none; - } - - @include ltemd { - gap: $space-xxl; - } -} - -.courses-list-filters { - display: flex; - margin-left: auto; - - gap: $space-xxl; - - > * { - min-width: 326px; - } - - > :global(.input-wrapper) { - width: 100%; - - > :global(.input-el) { - margin: 0; - } - } - - @include ltelg { - flex-direction: column; - align-items: flex-start; - gap: $space-lg; - } - - @include ltemd { - width: 100%; - > * { - min-width: auto; - } - } -} - -.title-line { - display: flex; - align-items: center; - - > h3 { - display: flex; - align-items: center; - gap: $space-sm; - } - - @include ltemd { - flex-direction: column; - align-items: flex-start; - gap: $space-xxl; - } -} diff --git a/src-ts/tools/learn/my-learning/tab-content-layout/TabContentLayout.tsx b/src-ts/tools/learn/my-learning/tab-content-layout/TabContentLayout.tsx deleted file mode 100644 index 7ef58b15c..000000000 --- a/src-ts/tools/learn/my-learning/tab-content-layout/TabContentLayout.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Dictionary, groupBy } from 'lodash' -import { ChangeEvent, ChangeEventHandler, Dispatch, FC, ReactNode, SetStateAction, useEffect, useMemo, useState } from 'react' - -import { InputSelect, useSessionStorage } from '../../../../lib' -import { LearnCertification } from '../../learn-lib' -import { SortOption } from '../my-learning-sort-options' - -import styles from './TabContentLayout.module.scss' - -interface TabContentLayoutProps { - certifications: ReadonlyArray - children: ReactNode - disableFilters?: boolean - onCategoryChange?: (category: string) => void - onSortChange?: (field: string) => void - sortOptions: ReadonlyArray - title: string -} - -const TabContentLayout: FC = (props: TabContentLayoutProps) => { - - const [sortingField, setSortingField]: [ - string, - Dispatch> - ] = useSessionStorage(`my-learn-sort[${props.title}]`, props.sortOptions[0].value) - - const [selectedCategory, setSelectedCategory]: [ - string, - Dispatch> - ] = useState('') - - // compute all the available category dropdown options - const certsCategoriesOptions: Array<{ - label: string, - value: string, - }> = useMemo(() => { - const certsByCategory: Dictionary> = groupBy(props.certifications, 'category') - return [ - { label: 'All Categories', value: '' }, - ...Object.keys(certsByCategory) - .sort() - .map(c => ({ - label: c, - value: c, - })), - ] - }, [props.certifications]) - - const handleCategoryChange: ChangeEventHandler = (e: ChangeEvent) => { - setSelectedCategory(e.target.value) - props.onCategoryChange?.(e.target.value) - } - - const handleSortChange: ChangeEventHandler = (e: ChangeEvent) => { - setSortingField(e.target.value) - props.onSortChange?.(e.target.value) - } - - useEffect(() => { - props.onCategoryChange?.(selectedCategory) - props.onSortChange?.(sortingField) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.onCategoryChange, props.onSortChange]) - - return ( -
    -
    -

    {props.title}

    - -
    - - -
    -
    - {props.children} -
    - ) -} - -export default TabContentLayout diff --git a/src-ts/tools/learn/my-learning/tab-content-layout/index.ts b/src-ts/tools/learn/my-learning/tab-content-layout/index.ts deleted file mode 100644 index c430b5485..000000000 --- a/src-ts/tools/learn/my-learning/tab-content-layout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as TabContentLayout } from './TabContentLayout' diff --git a/src-ts/tools/learn/my-learning/use-sort-and-filter.tsx b/src-ts/tools/learn/my-learning/use-sort-and-filter.tsx deleted file mode 100644 index f7d0b32fc..000000000 --- a/src-ts/tools/learn/my-learning/use-sort-and-filter.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { groupBy, mapValues, orderBy } from 'lodash' -import { Dispatch, SetStateAction, useMemo, useState } from 'react' - -import { LearnCertification, UserCertificationCompleted, UserCertificationInProgress } from '../learn-lib' - -type MyCertificationsType = ReadonlyArray - -export interface UseSortAndFilterValue { - certifications: MyCertificationsType, - handleCategoryChange: (category: string) => void, - handleSortChange: (field: string) => void, -} - -export const useSortAndFilter: ( - certifications: ReadonlyArray, - myCertifications: ReadonlyArray -) => UseSortAndFilterValue = ( - certifications, - myCertifications, -) => { - - const [selectedCategory, setSelectedCategory]: [ - string, - Dispatch> - ] = useState('') - - const [sortingField, setSortingField]: [ - string, - Dispatch> - ] = useState('') - - const [sortingDirection, setSortingDirection]: [ - string, - Dispatch> - ] = useState('asc') - - const handleSortChange: (field: string) => void = (field: string) => { - - setSortingField(field.replace(/^-/, '')) - setSortingDirection(field.startsWith('-') ? 'desc' : 'asc') - } - - const myCertificatesByCategory: {[key: string]: MyCertificationsType} = useMemo(() => { - - const certsById: {[key: string]: LearnCertification} = mapValues(groupBy(certifications, 'id'), ([cert]) => cert) - return groupBy( - myCertifications as MyCertificationsType, - cert => certsById[cert.certificationId]?.category, - ) - }, [certifications, myCertifications]) - - const myCertificationsFiltered: MyCertificationsType = ( - !selectedCategory ? myCertifications : myCertificatesByCategory[selectedCategory] - ) as MyCertificationsType ?? [] - - const myCertificationsSorted: MyCertificationsType = orderBy( - myCertificationsFiltered, - sortingField, - sortingDirection as 'asc'|'desc', - ) - - return { - certifications: myCertificationsSorted, - handleCategoryChange: setSelectedCategory, - handleSortChange, - - } -} diff --git a/src-ts/tools/learn/tca-certificate/certificate-not-found/CertificateNotFound.module.scss b/src-ts/tools/learn/tca-certificate/certificate-not-found/CertificateNotFound.module.scss new file mode 100644 index 000000000..6ea504f69 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-not-found/CertificateNotFound.module.scss @@ -0,0 +1,98 @@ +@import '../../../../lib/styles/includes'; + +.wrap { + display: flex; + height: 100%; + z-index: 1; + overflow: hidden; + + @include ltemd { + border: 2px dashed $blue-25; + border-radius: $space-sm; + } + + > svg { + position: absolute; + top: 50%; + left: 0; + width: 100%; + height: auto; + display: block; + z-index: -1; + transform: translateY(-50%); + @include ltemd { + display: none; + } + } +} + +.details { + width: 55%; + padding: calc($space-mx + $space-lg); + display: flex; + flex-direction: column; + flex: 1; + + @include ltemd { + flex-direction: row; + } +} + +.headerWrap { + display: flex; + justify-content: space-between; + align-items: flex-start; + + >svg { + width: 120px; + height: 120px; + } + +} + +.logos { + display: flex; + + &Divider { + width: 2px; + height: 40px; + background-color: $tc-white; + opacity: 0.3; + margin: 0 $space-lg; + } +} + +.contentWrap { + max-width: 528px; + margin-top: $space-mx; + + @include ltemd { + display: flex; + flex-direction: column; + > *:last-child { + margin-top: auto; + } + } + + h2 { + color: $blue-25; + } + + h3 { + font-size: 48px; + line-height: 50px; + font-weight: 500; + color: $tc-white; + margin-top: $space-sm; + } +} + +.mobileBadge { + margin: auto; + margin-right: 0; + width: 30%; + height: auto; + @include gtelg { + display: none; + } +} diff --git a/src-ts/tools/learn/tca-certificate/certificate-not-found/CertificateNotFound.tsx b/src-ts/tools/learn/tca-certificate/certificate-not-found/CertificateNotFound.tsx new file mode 100644 index 000000000..b81b12832 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-not-found/CertificateNotFound.tsx @@ -0,0 +1,42 @@ +import { FC } from 'react' +import classNames from 'classnames' + +import { TCAcademyLogoWhiteSvg, TCLogoSvg } from '../../../../lib' +import { CertificateNotFoundContent } from '../../learn-lib' + +import { ReactComponent as BackgroundSvg } from './bg.svg' +import { ReactComponent as BadgeSvg } from './badge.svg' +import styles from './CertificateNotFound.module.scss' + +const CertificateNotFound: FC<{}> = () => ( +
    + +
    +
    + +
    + +
    + +
    +
    +
    +

    Topcoder Academy

    +

    + Certificate +
    + not found +

    + +
    + +
    + +
    +
    + +
    +
    +) + +export default CertificateNotFound diff --git a/src-ts/tools/learn/tca-certificate/certificate-not-found/badge.svg b/src-ts/tools/learn/tca-certificate/certificate-not-found/badge.svg new file mode 100644 index 000000000..dadd53da9 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-not-found/badge.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src-ts/tools/learn/tca-certificate/certificate-not-found/bg.svg b/src-ts/tools/learn/tca-certificate/certificate-not-found/bg.svg new file mode 100644 index 000000000..30377b41b --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-not-found/bg.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src-ts/tools/learn/tca-certificate/certificate-not-found/index.ts b/src-ts/tools/learn/tca-certificate/certificate-not-found/index.ts new file mode 100644 index 000000000..a3ba843a9 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-not-found/index.ts @@ -0,0 +1 @@ +export { default as CertificateNotFound } from './CertificateNotFound' 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..1d638a8d8 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/certificate-view/CertificateView.tsx @@ -0,0 +1,107 @@ +import { FC, MutableRefObject, ReactNode, useCallback, useRef } from 'react' + +import { + IconOutline, + LoadingSpinner, + UserProfile, +} from '../../../../lib' +import { + ActionButton, + CertificateNotFoundContent, + CertificatePageLayout, + TCACertificatePreview, + TCACertification, + TCACertificationValidationData, + useValidateTCACertification, +} from '../../learn-lib' +import { getTCACertificationPath, getTCACertificationValidationUrl, getUserTCACertificateSsr } from '../../learn.routes' +import { CertificateNotFound } from '../certificate-not-found' + +interface CertificateViewProps { + certification: string, + fullScreenCertLayout?: boolean, + profile: UserProfile, +} + +const CertificateView: FC = (props: CertificateViewProps) => { + + const tcaCertificationPath: string = getTCACertificationPath(props.certification) + const certificateElRef: MutableRefObject = useRef() + + const { + certification, + enrollment, + error: hasValidationError, + ready, + }: TCACertificationValidationData + = useValidateTCACertification(props.certification, props.profile.handle) + + const hasCompletedTheCertification: boolean = !!certification && !!enrollment && !hasValidationError + const certificateNotFoundError: boolean = ready && !hasCompletedTheCertification + + function getCertTitle(user: string): string { + return `${user} - ${certification?.title}` + } + + const certUrl: string = getUserTCACertificateSsr( + props.certification, + props.profile.handle, + getCertTitle(props.profile.handle), + ) + + const certificationTitle: string = getCertTitle(enrollment?.userName || props.profile.handle) + + const validateLink: string = getTCACertificationValidationUrl(enrollment?.completionUuid as string) + + const handleLinkClick: () => void = useCallback(() => { + window.open(validateLink, 'blank') + }, [validateLink]) + + function renderCertificate(): ReactNode { + if (certificateNotFoundError) { + return + } + + return ( + + ) + } + + return ( + <> + + + } + onClick={handleLinkClick} + /> + )} + className={certificateNotFoundError ? 'cert-not-found-layout' : ''} + afterContent={certificateNotFoundError && ( + + )} + > + {renderCertificate()} + + + ) +} + +export default CertificateView 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/index.ts b/src-ts/tools/learn/tca-certificate/index.ts new file mode 100644 index 000000000..1831b9fd8 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/index.ts @@ -0,0 +1,4 @@ +export * from './my-certificate' +export * from './certificate-view' +export * from './user-certificate' +export * from './validate-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..b7bdfeec9 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/my-certificate/MyTCACertificate.tsx @@ -0,0 +1,44 @@ +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/UserTCACertificate.tsx b/src-ts/tools/learn/tca-certificate/user-certificate/UserTCACertificate.tsx new file mode 100644 index 000000000..548b0f4eb --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/user-certificate/UserTCACertificate.tsx @@ -0,0 +1,54 @@ +import { + Dispatch, + FC, + SetStateAction, + useEffect, + useState, +} from 'react' +import { Params, useParams } from 'react-router-dom' + +import { + LoadingSpinner, + profileGetPublicAsync, + UserProfile, +} from '../../../../lib' +import { CertificateView } from '../certificate-view' + +const UserTCACertificate: FC<{}> = () => { + + const routeParams: Params = useParams() + + const [profile, setProfile]: [ + UserProfile | undefined, + Dispatch> + ] = useState() + const [profileReady, setProfileReady]: [boolean, Dispatch>] = useState(false) + + const certificationParam: string = routeParams.certification ?? '' + + useEffect(() => { + if (routeParams.memberHandle) { + profileGetPublicAsync(routeParams.memberHandle) + .then(userProfile => { + setProfile(userProfile) + setProfileReady(true) + }) + } + }, [routeParams.memberHandle, setProfileReady]) + + return ( + <> + + + {profileReady && profile && ( + + )} + + ) +} + +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 new file mode 100644 index 000000000..58bd97b52 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/user-certificate/index.ts @@ -0,0 +1 @@ +export { default as UserTCACertificate } from './UserTCACertificate' diff --git a/src-ts/tools/learn/tca-certificate/validate-certificate/ValidateTCACertificate.module.scss b/src-ts/tools/learn/tca-certificate/validate-certificate/ValidateTCACertificate.module.scss new file mode 100644 index 000000000..0210305e3 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/validate-certificate/ValidateTCACertificate.module.scss @@ -0,0 +1,227 @@ +@import '../../../../lib/styles/includes'; + +.hero { + display: flex; + flex-direction: column; + padding-top: calc($space-mx * 2); + color: $tc-white; + + @include ltelg { + padding-top: $space-mx; + } + + &-dev { + background: url('./bg-curve-white.png') no-repeat center bottom / 100vw, $tc-dev-grad; + } + + &-datascience { + background: url('./bg-curve-white.png') no-repeat center bottom / 100vw, $tc-datascience-grad; + } + + &-design { + background: url('./bg-curve-white.png') no-repeat center bottom / 100vw, $tc-design-grad; + } + + &-qa { + background: url('./bg-curve-white.png') no-repeat center bottom / 100vw, $tc-qa-grad; + } + + .heroInner { + display: flex; + gap: $space-mx; + margin-bottom: calc($space-mx * 2); + overflow: hidden; + justify-content: center; + + @include ltelg { + flex-direction: column; + } + + .heroLeft { + display: flex; + flex-direction: column; + max-width: 750px; + flex: 1 1 auto; + + .member { + display: flex; + align-items: center; + margin-bottom: $space-mx; + + >svg, + >img { + max-width: 120px; + max-height: 120px; + } + + >img { + border: 3px solid $tc-white; + border-radius: 50%; + aspect-ratio: 1; + } + + .memberInfo { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-left: $space-lg; + + :global(.body-large-bold) { + font-size: 32px; + margin-bottom: $space-sm; + } + + .verifyStatus { + display: flex; + align-items: center; + min-width: 185px; + margin-top: $space-sm; + + >svg { + max-width: 24px; + } + + span { + margin: 0 $space-sm; + } + + .toolTipIcon { + width: 16px; + height: 16px; + cursor: pointer; + } + } + } + } + + .certTitle { + font-family: $font-barlow-condensed; + font-weight: 600; + font-size: 64px; + line-height: 64px; + text-transform: uppercase; + margin-top: $space-lg; + } + } + + .heroCert { + filter: drop-shadow(0px 20px 36px rgba(0, 0, 0, 0.22)); + flex: 1 1 auto; + min-width: 360px; + max-width: 480px; + @include ltelg { + max-width: none; + } + } + } +} + +.wrap { + display: flex; + flex-direction: column; + margin-top: calc($space-mx + $space-mx); + + @include ltelg { + margin-top: $space-mx; + } + + h2 { + font-family: $font-barlow; + } + + ul { + list-style: disc; + margin: $space-xxl 0 $space-mx $space-xxl; + } + + .courses { + display: flex; + flex-direction: column; + padding: $space-xl $space-xxxxl; + background: $black-5; + border-radius: $space-sm; + margin-bottom: $space-mx; + + .coursesGrid { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: $space-xl; + margin-top: $space-xxl; + + @include ltelg { + grid-template-columns: 1fr 1fr; + } + + @include ltesm { + grid-template-columns: 1fr; + } + + .courseCard { + background-color: $tc-white; + border-radius: $space-sm; + padding: $space-lg; + display: flex; + align-items: center; + + svg { + width: 48px; + height: 48px; + margin-right: $space-lg; + } + } + } + } +} + +.modalView { + .hero { + padding-top: $space-mx; + max-height: 440px; + min-height: 380px; + + @include ltesm { + max-height: none; + } + + + .heroInner { + margin-bottom: 0; + flex-direction: row; + padding-right: $space-mx; + + @include ltesm { + flex-direction: column; + padding-right: 0; + } + + .heroLeft { + .member { + margin-bottom: $space-xxxxl; + + >svg, + >img { + max-width: 88px; + max-height: 88px; + } + } + + .certTitle { + font-size: 40px; + line-height: 40px; + } + } + } + } + + .contentOuter { + max-width: none; + + >div { + max-width: none !important; + } + + .courses { + margin-bottom: 0; + } + } +} \ No newline at end of file diff --git a/src-ts/tools/learn/tca-certificate/validate-certificate/ValidateTCACertificate.tsx b/src-ts/tools/learn/tca-certificate/validate-certificate/ValidateTCACertificate.tsx new file mode 100644 index 000000000..20ed45306 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/validate-certificate/ValidateTCACertificate.tsx @@ -0,0 +1,201 @@ +import { + Dispatch, + FC, + MutableRefObject, + ReactNode, + SetStateAction, + useEffect, + useLayoutEffect, + useMemo, useRef, + useState, +} from 'react' +import { Params, useParams, useSearchParams } from 'react-router-dom' +import classNames from 'classnames' + +import { + Button, + ContentLayout, + DefaultMemberIcon, + IconOutline, + LoadingSpinner, + profileGetPublicAsync, + Tooltip, + UserProfile, + VerifiedMemberFlagSvg, +} from '../../../../lib' +import { + CourseBadge, + TCACertificatePreview, + TCACertificateType, + TCACertification, + TCACertificationEnrollmentProviderData, + useTCACertificationEnrollment, +} from '../../learn-lib' +import { EnvironmentConfig } from '../../../../config' +import { getTCACertificationValidationUrl } from '../../learn.routes' +import { clearFCCCertificationTitle, hideSiblings } from '../../learn-lib/functions' + +import styles from './ValidateTCACertificate.module.scss' + +const ValidateTCACertificate: FC<{}> = () => { + + const wrapElRef: MutableRefObject = useRef() + + const routeParams: Params = useParams() + const [queryParams]: [URLSearchParams, any] = useSearchParams() + + const isModalView: boolean = queryParams.get('view-style') === 'modal' + + const [profile, setProfile]: [ + UserProfile | undefined, + Dispatch> + ] = useState() + + const [profileReady, setProfileReady]: [boolean, Dispatch>] = useState(false) + + const { + enrollment, + ready: certReady, + }: TCACertificationEnrollmentProviderData + = useTCACertificationEnrollment(routeParams.completionUuid as string) + + const certification: TCACertification | undefined = enrollment?.topcoderCertification + + const courses: any = certification?.certificationResources + + const learnedOutcomes: ReactNode[] | undefined + = useMemo(() => certification?.learnedOutcomes?.map((lO: string) =>
  • {lO}
  • ), [certification]) + + const coursesGridItems: ReactNode[] | undefined + = useMemo(() => courses?.map((course: any) => ( +
    + +

    + {clearFCCCertificationTitle(course.freeCodeCampCertification.title)} +

    +
    + )), [courses, certification]) + + // TODO: update this to use `completionUuid` + const validateLink: string + = getTCACertificationValidationUrl(routeParams.completionUuid as string) + + useEffect(() => { + if (enrollment?.userHandle) { + profileGetPublicAsync(enrollment.userHandle) + .then(userProfile => { + setProfile(userProfile) + setProfileReady(true) + }) + } + }, [enrollment, setProfileReady]) + + useLayoutEffect(() => { + const el: HTMLElement = wrapElRef.current + if (!el || !isModalView) { + return + } + + hideSiblings(el) + hideSiblings(el.parentElement as HTMLElement) + + }) + + function visitFullProfile(): void { + window.open(`${EnvironmentConfig.TOPCODER_URLS.USER_PROFILE}/${profile?.handle}`, '_blank') + } + + return ( + <> + + + {profile && certification && ( +
    +
    + +
    +
    +
    + { + profile.photoURL ? ( + Member Avatar + ) : ( + + ) + } +
    +

    {enrollment?.userName}

    +

    {profile.handle}

    +
    + + verified member + + )} + content={`This member is compliant with Topcoder policies + and is a trusted member of the Topcoder community.`} + /> +
    +
    +
    +

    + {enrollment?.userName} + {' '} + has successfully completed the certification +

    +
    {certification.title}
    +
    +
    + +
    +
    +
    +
    + + +
    +

    + {'What '} + {enrollment?.userName} + {' Learned?'} +

    +
      {learnedOutcomes}
    + +
    +

    Courses Taken

    +
    + {coursesGridItems} +
    +
    + { + !isModalView && ( +
    +
    +
    + )} + + ) +} + +export default ValidateTCACertificate diff --git a/src-ts/tools/learn/tca-certificate/validate-certificate/bg-curve-white.png b/src-ts/tools/learn/tca-certificate/validate-certificate/bg-curve-white.png new file mode 100644 index 000000000..c2f3aa271 Binary files /dev/null and b/src-ts/tools/learn/tca-certificate/validate-certificate/bg-curve-white.png differ diff --git a/src-ts/tools/learn/tca-certificate/validate-certificate/index.ts b/src-ts/tools/learn/tca-certificate/validate-certificate/index.ts new file mode 100644 index 000000000..cbf26bfa4 --- /dev/null +++ b/src-ts/tools/learn/tca-certificate/validate-certificate/index.ts @@ -0,0 +1 @@ +export { default as ValidateTCACertificate } from './ValidateTCACertificate' diff --git a/src-ts/tools/learn/welcome/WelcomePage.tsx b/src-ts/tools/learn/welcome/WelcomePage.tsx index cb154b27e..1bb0f8125 100644 --- a/src-ts/tools/learn/welcome/WelcomePage.tsx +++ b/src-ts/tools/learn/welcome/WelcomePage.tsx @@ -1,11 +1,25 @@ -import { FC } from 'react' +import { Dictionary, groupBy, identity, orderBy } from 'lodash' +import { ChangeEvent, Dispatch, FC, SetStateAction, useCallback, useContext, useMemo } from 'react' import classNames from 'classnames' import { PageSubheaderPortalId } from '../../../config' -import { ContentLayout, LoadingSpinner, Portal } from '../../../lib' +import { + ContentLayout, + LoadingSpinner, + PageDivider, + Portal, + profileContext, + ProfileContextData, + useLocalStorage, +} from '../../../lib' import { AllCertificationsProviderData, + LearnCertification, + TCACertificationsProgressProviderData, + TCACertificationsProviderData, useGetAllCertifications, + useGetAllTCACertifications, + useGetAllTCACertificationsProgress, useGetUserCertifications, UserCertificationsProviderData, WaveHero, @@ -13,17 +27,61 @@ import { import '../../../lib/styles/index.scss' import { AvailableCoursesList } from './available-courses-list' -import { ProgressBlock } from './progress-block' -import { ReactComponent as TcAcademyFullLogoSvg } from './tca-full-logo.svg' +import { WhatTCACanDo } from './what-tca-cando' +import { TCCertifications } from './tc-certifications' import styles from './WelcomePage.module.scss' +const PRIORITY_CATEGORIES: ReadonlyArray = [ + 'Data Science', + 'Web Development', +] + const WelcomePage: FC = () => { + const { initialized: profileReady, profile }: ProfileContextData = useContext(profileContext) const allCertsData: AllCertificationsProviderData = useGetAllCertifications() const userCertsData: UserCertificationsProviderData = useGetUserCertifications() const coursesReady: boolean = allCertsData.ready && userCertsData.ready + const allTCACertifications: TCACertificationsProviderData = useGetAllTCACertifications() + + const { + progresses: certsProgress, + ready: progressReady, + }: TCACertificationsProgressProviderData = useGetAllTCACertificationsProgress( + profile?.userId as unknown as string, + { enabled: profileReady && !!profile }, + ) + + const ready: boolean = profileReady && coursesReady && (!profile || progressReady) + + const [selectedCategory, setSelectedCategory]: [ + string, + Dispatch> + ] = useLocalStorage('tca-welcome-filter-certs', '') + + // certificates indexed by category, sorted by title + const certsByCategory: Dictionary> + = useMemo(() => ( + groupBy(orderBy(allCertsData.certifications, 'title', 'asc'), 'certificationCategory.category') + ), [allCertsData.certifications]) + + // create and sort the certificates groups + const certificationsGroups: Array = useMemo(() => orderBy( + Object.keys(certsByCategory), + [ + c => (PRIORITY_CATEGORIES.includes(c) ? -1 : 1), + identity, + ], + ['asc', 'asc'], + ), [certsByCategory]) + + const onSelectCategory: (e: ChangeEvent) => void + = useCallback((e: ChangeEvent) => { + setSelectedCategory(e.target.value as string) + }, [setSelectedCategory]) + return ( @@ -33,39 +91,39 @@ const WelcomePage: FC = () => {
    - - Welcome! - + <>Topcoder Academy )} text={` The Topcoder Academy will provide you with learning opportunities in the form of guided learning paths. You will have the opportunity to learn new skills that will better - prepare you to earn on the Topcoder platform.
    -
    - We look forward to learning with you! + prepare you to earn on the Topcoder platform. `} theme='light' - > - -
    + />
    - + + + + + + + {coursesReady && ( )}
    diff --git a/src-ts/tools/learn/welcome/available-courses-list/AvailableCoursesList.module.scss b/src-ts/tools/learn/welcome/available-courses-list/AvailableCoursesList.module.scss index 1294fd777..26bdecd35 100644 --- a/src-ts/tools/learn/welcome/available-courses-list/AvailableCoursesList.module.scss +++ b/src-ts/tools/learn/welcome/available-courses-list/AvailableCoursesList.module.scss @@ -1,96 +1,86 @@ @import '../../../../lib/styles/includes'; -.courses-list { - flex-wrap: wrap; - gap: $space-lg; - display: grid; - grid-template-columns: repeat(4, 1fr); - margin-bottom: $space-xxl; - - // NOTE: these are not app defined breakpoints - // but they are required for the grid to look at it's best - @media (max-width: 1200px) { - grid-template-columns: repeat(3, 1fr); - } - - @include ltelg { - grid-template-columns: repeat(2, 1fr); - } - - @media (max-width: 576px) { - grid-template-columns: repeat(1, 1fr); - } +.coursesListHeaderWrap { + display: flex; + flex-direction: column; - + .courses-group-title { - margin-top: $space-mxx; + .teaseBanner { + display: flex; + flex-direction: column; + position: relative; + padding: $space-xxxxl; + margin: $space-xxl 0 $space-mx; + background-image: url("./assets/courses-banner-bg.png"); + background-size: cover; + border-radius: 8px; @include ltemd { - margin-top: $space-xxxxl; + padding: 88px $space-lg $space-lg; + margin: 0 0 $space-xxl; + background-image: url("./assets/courses-banner-bg-mobile.png"); } - } -} -.courses-group-title { - margin-top: $space-xxl; - padding: $space-xxl 0; - border-top: $border solid $black-10; + >h2 { + color: $tc-white; + margin-bottom: $space-lg; + font-family: $font-barlow; - @include ltemd { - padding: $space-lg 0; - } -} + @include ltemd { + font-size: 24px; + } + } -.courses-list-header { - display: flex; - align-items: center; + >p { + color: $tc-white; + max-width: $md-max; + } + } - > h3 { + .coursesListHeader { display: flex; + justify-content: space-between; align-items: center; - gap: $space-sm; - } - - @include ltemd { - flex-direction: column; - align-items: flex-start; - gap: $space-xxl; - } -} - -.badge { - font-family: $font-roboto; - background: $blue-100; - - padding: 0 $space-sm; - border-radius: 50px; - color: $tc-white; -} + padding-bottom: $space-lg; -.courses-list-filters { - display: flex; - margin-left: auto; + @include ltemd { + flex-direction: column; + align-items: flex-start; + padding-bottom: $space-xxl; - gap: $space-xxl; + h2 { + margin: $space-md 0 $space-xxl; + } + } - > * { - min-width: 326px; - } + h2 { + display: flex; + align-items: center; - > :global(.input-wrapper) { - width: 100%; + .badge { + font-family: $font-roboto; + background: $blue-100; - > :global(.input-el) { - margin: 0; + padding: 0 $space-sm; + margin-left: $space-sm; + border-radius: 50px; + color: $tc-white; + } } } +} - @include ltelg { - flex-direction: column; - align-items: flex-start; - gap: $space-lg; +.coursesList { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: $space-xl; + padding: $space-xxl 0 $space-mx; + + &:last-child { + border-bottom: none; } @include ltemd { - width: 100%; + grid-template-columns: 1fr; + padding: $space-lg 0 $space-xxl; } -} +} \ No newline at end of file diff --git a/src-ts/tools/learn/welcome/available-courses-list/AvailableCoursesList.tsx b/src-ts/tools/learn/welcome/available-courses-list/AvailableCoursesList.tsx index 0880424e3..c2ab37bd4 100644 --- a/src-ts/tools/learn/welcome/available-courses-list/AvailableCoursesList.tsx +++ b/src-ts/tools/learn/welcome/available-courses-list/AvailableCoursesList.tsx @@ -1,105 +1,91 @@ -import { Dictionary, groupBy, identity, orderBy } from 'lodash' -import { Dispatch, FC, Fragment, ReactNode, SetStateAction, useMemo } from 'react' +import { Dictionary } from 'lodash' +import { ChangeEvent, FC, Fragment, ReactNode, useMemo } from 'react' import classNames from 'classnames' -import { InputSelect, useLocalStorage } from '../../../../lib' -import { LearnCertification, UserCertificationCompleted, UserCertificationInProgress } from '../../learn-lib' +import { LearnCertification, LearnUserCertificationProgress } from '../../learn-lib' import { CoursesCard } from '../courses-card' +import { FilterBar } from '../filter-bar' import styles from './AvailableCoursesList.module.scss' interface AvailableCoursesListProps { + certsByCategory: Dictionary> certifications: ReadonlyArray - userCompletedCertifications: ReadonlyArray - userInProgressCertifications: ReadonlyArray + certificationsGroups: Array + selectedCategory: string + certificationsProgresses: ReadonlyArray + onSelectCategory: (e: ChangeEvent) => void } -const PRIORITY_CATEGORIES: ReadonlyArray = [ - 'Data Science', - 'Web Development', -] - const AvailableCoursesList: FC = (props: AvailableCoursesListProps) => { - - const [selectedCategory, setSelectedCategory]: [ - string, - Dispatch> - ] = useLocalStorage('tca-welcome-filter-certs', '') - - // certificates indexed by category, sorted by title - const certsByCategory: Dictionary> = useMemo(() => groupBy(orderBy(props.certifications, 'title', 'asc'), 'category'), [props.certifications]) - - // compute all the available category dropdown options - const certsCategoriesOptions: Array<{ - label: string, - value: string, - }> = useMemo(() => [ - { label: 'All Categories', orderIndex: -1, value: '' }, - ...Object.keys(certsByCategory) - .sort() - .map(c => ({ - label: c, - value: c, - })), - ], [certsByCategory]) - - // create and sort the certificates groups - const certificationsGroups: Array = useMemo(() => orderBy( - Object.keys(certsByCategory), - [ - c => (PRIORITY_CATEGORIES.includes(c) ? -1 : 1), - identity, - ], - ['asc', 'asc'], - ), [certsByCategory]) - const certificationsCount: number = ( - (certsByCategory[selectedCategory] ?? props.certifications).length + (props.certsByCategory[props.selectedCategory] ?? props.certifications).length ) - const renderCertificationGroup = (category: string): ReactNode => ( + const renderCertificationGroup: (category: string) => ReactNode = (category: string) => (

    {category}

    -
    - {certsByCategory[category] +
    + {props.certsByCategory[category] .map(certification => ( p.certificationId === certification.fccId) + } /> ))}
    ) + // compute all the available category dropdown options + const certsCategoriesOptions: Array<{ + label: string, + value: string, + }> = useMemo(() => [ + { label: 'All Categories', orderIndex: -1, value: '' }, + ...Object.keys(props.certsByCategory) + .sort() + .map(c => ({ + label: c, + value: c, + })), + ], [props.certsByCategory]) + return (
    -
    -

    - Courses Available - - {certificationsCount} - -

    - -
    - setSelectedCategory(e.target.value as string)} - name='filter-courses' - label='Categories' +
    +
    +

    + Courses + + {certificationsCount} + +

    + +
    + +
    +

    Check out our Courses

    +

    + Topcoder is partnering with multiple content providers + to bring you a best in class course catalog. Stay tuned for more courses! +

    +
    - {certificationsGroups.map(category => ( - (!selectedCategory || selectedCategory === category) + {props.certificationsGroups.map(category => ( + (!props.selectedCategory || props.selectedCategory === category) && renderCertificationGroup(category) ))}
    diff --git a/src-ts/tools/learn/welcome/available-courses-list/assets/courses-banner-bg-mobile.png b/src-ts/tools/learn/welcome/available-courses-list/assets/courses-banner-bg-mobile.png new file mode 100644 index 000000000..531bb7187 Binary files /dev/null and b/src-ts/tools/learn/welcome/available-courses-list/assets/courses-banner-bg-mobile.png differ diff --git a/src-ts/tools/learn/welcome/available-courses-list/assets/courses-banner-bg.png b/src-ts/tools/learn/welcome/available-courses-list/assets/courses-banner-bg.png new file mode 100644 index 000000000..3387f37bf Binary files /dev/null and b/src-ts/tools/learn/welcome/available-courses-list/assets/courses-banner-bg.png differ diff --git a/src-ts/tools/learn/welcome/courses-card/CoursesCard.module.scss b/src-ts/tools/learn/welcome/courses-card/CoursesCard.module.scss index 019b1ea12..305b31b53 100644 --- a/src-ts/tools/learn/welcome/courses-card/CoursesCard.module.scss +++ b/src-ts/tools/learn/welcome/courses-card/CoursesCard.module.scss @@ -5,35 +5,130 @@ border-radius: $space-sm; padding: $space-xxl; - gap: $space-lg; + gap: $space-sm; display: flex; flex-direction: column; width: 100%; - :global(.overline) { - color: $black-60; + @include ltemd { + padding: $space-lg; + min-height: auto; } +} + +.completed { + background: $tc-white; + border: 2px solid $turq-75; +} - &:global(.soon) { - :global(.badge-icon) { - opacity: 0.5; +.cardHeader { + display: flex; + + >div svg { + max-width: 48px; + max-height: 48px; + + @include ltemd { + max-width: 40px; + max-height: 40px; } } - @include ltemd { - padding: $space-lg; + .cardHeaderTitleWrap { + display: flex; + flex-direction: column; + margin-left: $space-lg; + + @include ltemd { + margin-left: $space-sm; + } + + .subTitleWrap { + display: flex; + align-items: center; + + @include ltesm { + display: grid; + grid-template-columns: 30px 1fr; + } + + >svg { + color: $blue-140; + margin-right: $space-xs; + } + + >em { + font-style: italic; + font-size: 14px; + line-height: 22px; + color: $black-60; + margin-right: $space-lg; + + &:last-child { + margin-right: 0; + } + } + } } } -.text { - color: $black-100; - :global(.quote-small) { - color: #767676; +.cardHeaderDividerWrap { + margin-top: auto; + margin-bottom: $space-lg; + padding-top: $space-sm; + + .cardHeaderDivider { + height: 1px; + border-bottom: $border solid $black-10; + width: 100%; } } -.bottom { +.cardBody { + display: flex; + flex-direction: column; + + .certProvider { + display: flex; + align-items: center; + color: $black-60; + font-style: italic; + font-size: 14px; + line-height: 22px; + + >svg { + margin-left: $space-sm; + fill: #0A0A23; + } + } +} + +.cardBottom { color: $blue-140; - margin-top: auto; + + a:nth-child(2) { + margin-left: $space-sm; + } + + .completedCTAs { + display: flex; + justify-content: space-between; + align-items: flex-end; + + @include ltexs { + flex-direction: column; + align-items: flex-start; + } + } +} + +.infoText { + font-style: italic; + color: $black-60; + margin-right: $space-sm; + + &:last-child { + margin-right: 0; + } } diff --git a/src-ts/tools/learn/welcome/courses-card/CoursesCard.tsx b/src-ts/tools/learn/welcome/courses-card/CoursesCard.tsx index 0f1364e81..342f40ade 100644 --- a/src-ts/tools/learn/welcome/courses-card/CoursesCard.tsx +++ b/src-ts/tools/learn/welcome/courses-card/CoursesCard.tsx @@ -1,12 +1,18 @@ -import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react' +import { FC, memo, ReactNode } from 'react' import classNames from 'classnames' -import { Button, ButtonStyle } from '../../../../lib' +import { Button, FccLogoBlackSvg, IconSolid, ProgressBar } from '../../../../lib' import { - CourseTitle, + clearFCCCertificationTitle, + CompletionTimeRange, + CourseBadge, LearnCertification, - UserCertificationCompleted, - UserCertificationInProgress, + LearnLevelIcon, + LearnUserCertificationProgress, + SkillTags, + TCACertificationCompletionTimeRange, + useHoursEstimateToRange, + UserCertificationProgressStatus, } from '../../learn-lib' import { getCertificatePath, getCoursePath, getLessonPathFromCurrentLesson } from '../../learn.routes' @@ -14,90 +20,106 @@ import styles from './CoursesCard.module.scss' interface CoursesCardProps { certification: LearnCertification - userCompletedCertifications: ReadonlyArray - userInProgressCertifications: ReadonlyArray + progress?: LearnUserCertificationProgress } +const EXCERPT_TEXT_LEN: number = 95 + const CoursesCard: FC = (props: CoursesCardProps) => { + const desc: string = props.certification.description?.slice(0, EXCERPT_TEXT_LEN) + const descLength: number = props.certification.description?.length + const courseEnabled: boolean = props.certification.state === 'active' + const isCompleted: boolean = props.progress?.status === UserCertificationProgressStatus.completed + const isInProgress: boolean = props.progress?.status === UserCertificationProgressStatus.inProgress - const [buttonStyle, setButtonStyle]: [ButtonStyle, Dispatch>] - = useState('primary') - const [buttonLabel, setButtonLabel]: [string, Dispatch>] - = useState('') - const [link, setLink]: [string, Dispatch>] - = useState('') + function renderCtaBtns(status?: UserCertificationProgressStatus): ReactNode { + const provider: string = props.certification.resourceProvider.name + const certificationCourse: string = props.certification.certification + const currentLesson: string | undefined = props.progress?.currentLesson - const courseEnabled: boolean = props.certification.state === 'active' - useEffect(() => { + const resumeRoute: string = getLessonPathFromCurrentLesson(provider, certificationCourse, currentLesson) + const detailsRoute: string = getCoursePath(provider, certificationCourse) + const certifRoute: string = getCertificatePath(provider, certificationCourse) - // if the course isn't enabled, there's nothing to do - if (!courseEnabled) { - return - } + switch (status) { + case UserCertificationProgressStatus.completed: + return ( +
    +
    +
    +
    + ) + case UserCertificationProgressStatus.inProgress: + return
    ) } diff --git a/src-ts/utils/settings/account/change-password/ChangePassword.tsx b/src-ts/utils/settings/account/change-password/ChangePassword.tsx deleted file mode 100644 index 37f967151..000000000 --- a/src-ts/utils/settings/account/change-password/ChangePassword.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Dispatch, FC, SetStateAction, useCallback, useContext, useState } from 'react' - -import { - ChangePasswordRequest, - Form, - FormDefinition, - formGetInputModel, - FormInputModel, - profileContext, - ProfileContextData, - UserProfile, -} from '../../../../lib' - -import { ChangePasswordFieldName, changePasswordFormDef } from './change-password-form.config' - -interface ChangePasswordProps { - readonly onClose: () => void -} - -const ChangePassword: FC = (props: ChangePasswordProps) => { - - const profileContextData: ProfileContextData = useContext(profileContext) - const { profile, changePassword }: ProfileContextData = profileContextData - - const [passwordForm]: [FormDefinition, Dispatch>] - = useState(changePasswordFormDef) - - const requestGenerator = useCallback(( - inputs: ReadonlyArray, - ): ChangePasswordRequest => { - const password: string - = formGetInputModel(inputs, ChangePasswordFieldName.currentPassword).value as string - const newPassword: string - = formGetInputModel(inputs, ChangePasswordFieldName.newPassword).value as string - return { - newPassword, - password, - } - }, []) - - const save = useCallback((updatedPassword: ChangePasswordRequest): Promise => ( - changePassword((profile as UserProfile).userId, updatedPassword) - .then(() => { - props.onClose() - }) - ), [changePassword, profile, props.onClose]) - - return ( -
    - ) -} - -export default ChangePassword diff --git a/src-ts/utils/settings/account/change-password/change-password-form.config.ts b/src-ts/utils/settings/account/change-password/change-password-form.config.ts deleted file mode 100644 index 8d79ffc5b..000000000 --- a/src-ts/utils/settings/account/change-password/change-password-form.config.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - FormDefinition, - FormInputAutocompleteOption, - validatorDoesNotMatchOther, - validatorMatchOther, - validatorPassword, - validatorRequired, - validatorRequiredIfOther, -} from '../../../../lib' - -export const changePasswordFormTitle: string = 'Change Password' - -export enum ChangePasswordFieldName { - confirmPassword = 'confirmPassword', - currentPassword = 'password', - newPassword = 'newPassword', -} - -export const changePasswordFormDef: FormDefinition = { - buttons: { - primaryGroup: [ - { - buttonStyle: 'secondary', - isSubmit: true, - label: 'Change password', - size: 'xl', - type: 'submit', - }, - ], - }, - groups: [ - { - inputs: [ - { - autocomplete: FormInputAutocompleteOption.current, - dependentFields: [ - ChangePasswordFieldName.newPassword, - ], - label: 'Current Password', - name: ChangePasswordFieldName.currentPassword, - placeholder: 'Enter your current password', - type: 'password', - validators: [ - { - validator: validatorRequired, - }, - ], - }, - { - autocomplete: FormInputAutocompleteOption.new, - dependentFields: [ - ChangePasswordFieldName.confirmPassword, - ChangePasswordFieldName.currentPassword, - ], - label: 'New Password', - name: ChangePasswordFieldName.newPassword, - placeholder: 'Enter your new password', - type: 'password', - validators: [ - { - validator: validatorRequired, - }, - { - dependentField: ChangePasswordFieldName.currentPassword, - validator: validatorDoesNotMatchOther, - }, - { - validator: validatorPassword, - }, - ], - }, - { - autocomplete: FormInputAutocompleteOption.off, - dependentFields: [ - ChangePasswordFieldName.newPassword, - ], - label: 'Confirm Password', - name: ChangePasswordFieldName.confirmPassword, - placeholder: 'Re-enter your new password', - type: 'password', - validators: [ - { - dependentField: ChangePasswordFieldName.newPassword, - validator: validatorRequiredIfOther, - }, - { - dependentField: ChangePasswordFieldName.newPassword, - validator: validatorMatchOther, - }, - ], - }, - ], - }, - ], - shortName: 'Password', - tabIndexStart: 3, - title: changePasswordFormTitle, -} diff --git a/src-ts/utils/settings/account/change-password/index.ts b/src-ts/utils/settings/account/change-password/index.ts deleted file mode 100644 index d1267be44..000000000 --- a/src-ts/utils/settings/account/change-password/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as ChangePassword } from './ChangePassword' -export { changePasswordFormTitle, changePasswordFormDef } from './change-password-form.config' diff --git a/src-ts/utils/settings/account/edit-name/EditName.tsx b/src-ts/utils/settings/account/edit-name/EditName.tsx deleted file mode 100644 index 3af3a36dc..000000000 --- a/src-ts/utils/settings/account/edit-name/EditName.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Dispatch, FC, SetStateAction, useCallback, useContext, useState } from 'react' - -import { - EditNameRequest, - Form, - FormDefinition, - formGetInputModel, - FormInputModel, - profileContext, - ProfileContextData, - UserProfile, -} from '../../../../lib' -import '../../../../lib/styles/index.scss' - -import { EditNameFieldName, editNameFormDef } from './edit-name-form.config' - -interface EditNameProps { - onClose: () => void -} - -const EditName: FC = (props: EditNameProps) => { - - const profileContextData: ProfileContextData = useContext(profileContext) - const { profile, updateProfile }: ProfileContextData = profileContextData - - const [profileForm]: [FormDefinition, Dispatch>] - = useState(editNameFormDef) - - const requestGenerator = useCallback(( - inputs: ReadonlyArray, - ): EditNameRequest => { - const firstName: string - = formGetInputModel(inputs, EditNameFieldName.firstName).value as string - const lastName: string - = formGetInputModel(inputs, EditNameFieldName.lastName).value as string - return { - firstName, - lastName, - } - }, []) - - const saveProfile = useCallback((updatedProfile: EditNameRequest): Promise => ( - updateProfile({ - ...profileContextData, - profile: { - ...profileContextData.profile as UserProfile, - firstName: updatedProfile.firstName, - lastName: updatedProfile.lastName, - }, - }) - .then(() => { - props.onClose() - }) - ), [profileContextData, props.onClose, updateProfile]) - - return ( - - ) -} - -export default EditName diff --git a/src-ts/utils/settings/account/edit-name/edit-name-form.config.ts b/src-ts/utils/settings/account/edit-name/edit-name-form.config.ts deleted file mode 100644 index f6da247ed..000000000 --- a/src-ts/utils/settings/account/edit-name/edit-name-form.config.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { FormDefinition, FormInputAutocompleteOption, validatorRequired } from '../../../../lib' - -export const editNameFormTitle: string = 'Edit Name' - -export enum EditNameFieldName { - firstName = 'firstName', - lastName = 'lastName', -} - -export const editNameFormDef: FormDefinition = { - buttons: { - primaryGroup: [ - { - buttonStyle: 'secondary', - isSubmit: true, - label: 'Save', - size: 'lg', - type: 'submit', - }, - ], - }, - groups: [ - { - inputs: [ - { - autocomplete: FormInputAutocompleteOption.off, - label: 'First Name', - name: EditNameFieldName.firstName, - type: 'text', - validators: [ - { - validator: validatorRequired, - }, - ], - }, - { - autocomplete: FormInputAutocompleteOption.off, - label: 'Last Name', - name: EditNameFieldName.lastName, - type: 'text', - validators: [ - { - validator: validatorRequired, - }, - ], - }, - ], - }, - ], - shortName: 'Name', - tabIndexStart: 3, - title: editNameFormTitle, -} diff --git a/src-ts/utils/settings/account/edit-name/index.ts b/src-ts/utils/settings/account/edit-name/index.ts deleted file mode 100644 index f5d882fc2..000000000 --- a/src-ts/utils/settings/account/edit-name/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as EditName } from './EditName' -export { editNameFormDef, editNameFormTitle } from './edit-name-form.config' diff --git a/src-ts/utils/settings/settings.routes.tsx b/src-ts/utils/settings/settings.routes.tsx index 728a06002..993fa1976 100644 --- a/src-ts/utils/settings/settings.routes.tsx +++ b/src-ts/utils/settings/settings.routes.tsx @@ -4,6 +4,10 @@ import { lazyLoad, LazyLoadedComponent, PlatformRoute } from '../../lib' const Account: LazyLoadedComponent = lazyLoad(() => import('./account'), 'Account') const Settings: LazyLoadedComponent = lazyLoad(() => import('./Settings')) +/** + * DEPRECATED + * TODO: Remove after some time, when clear no one links to here... + */ export const settingsRoutes: ReadonlyArray = [ { authRequired: true, diff --git a/src/index.jsx b/src/index.jsx index 4c1a62e81..5e9ffa5b8 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -31,45 +31,49 @@ logInitialize(EnvironmentConfig); const root = createRoot(document.getElementById("root")); root.render( -
    - - + <> + +