From 9f7ecffc0d2baee11595c0fff77f580b610346cd Mon Sep 17 00:00:00 2001 From: Anna Date: Mon, 29 Apr 2024 12:55:24 -0400 Subject: [PATCH] feat(learn): create a progress indicator map (#53963) Co-authored-by: sembauke --- client/src/assets/icons/completion-ribbon.tsx | 97 +++++++++++ client/src/components/Map/index.tsx | 161 ++++++++++++++++-- client/src/components/Map/map.css | 59 ++++++- client/src/components/layouts/global.css | 9 +- 4 files changed, 313 insertions(+), 13 deletions(-) create mode 100644 client/src/assets/icons/completion-ribbon.tsx diff --git a/client/src/assets/icons/completion-ribbon.tsx b/client/src/assets/icons/completion-ribbon.tsx new file mode 100644 index 00000000000..e3862542fdd --- /dev/null +++ b/client/src/assets/icons/completion-ribbon.tsx @@ -0,0 +1,97 @@ +import React from 'react'; + +interface RibbonProps { + value: number; + isClaimed: boolean; + isCompleted: boolean; +} + +export const Arrow = () => ( + + + + + +); + +export const RibbonIcon = ({ + value, + isCompleted: completed, + isClaimed +}: RibbonProps): JSX.Element => { + const properClassName = completed ? 'completeIcon' : 'incompleteIcon'; + const fillColor = completed ? 'black' : 'gray'; + return ( + + ); +}; + +RibbonIcon.displayName = 'RibbonIcon'; diff --git a/client/src/components/Map/index.tsx b/client/src/components/Map/index.tsx index aea3957d9d9..2b0e7b1d8e5 100644 --- a/client/src/components/Map/index.tsx +++ b/client/src/components/Map/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; - +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; import { SuperBlockStages, SuperBlocks, @@ -14,8 +15,24 @@ import { showUpcomingChanges } from '../../../config/env.json'; import './map.css'; +import { + isSignedInSelector, + currentCertsSelector +} from '../../redux/selectors'; + +import { RibbonIcon, Arrow } from '../../assets/icons/completion-ribbon'; + +import { CurrentCert, ClaimedCertifications } from '../../redux/prop-types'; +import { + certSlugTypeMap, + superBlockCertTypeMap +} from '../../../../shared/config/certification-settings'; + interface MapProps { forLanding?: boolean; + isSignedIn: boolean; + currentCerts: CurrentCert[]; + claimedCertifications?: ClaimedCertifications; } const linkSpacingStyle = { @@ -31,12 +48,31 @@ const coreCurriculum = [ ...superBlockOrder[SuperBlockStages.Python] ]; +const mapStateToProps = createSelector( + isSignedInSelector, + currentCertsSelector, + (isSignedIn: boolean, currentCerts) => ({ + isSignedIn, + currentCerts + }) +); + function MapLi({ superBlock, - landing = false + landing = false, + last = false, + trackProgress, + completed, + claimed, + index }: { superBlock: SuperBlocks; landing: boolean; + last?: boolean; + trackProgress: boolean; + completed: boolean; + claimed: boolean; + index: number; }) { return ( <> @@ -44,6 +80,19 @@ function MapLi({ data-test-label='curriculum-map-button' data-playwright-test-label='curriculum-map-button' > + {trackProgress && ( + <> +
+ +
+
{!last && }
+ + )} +
@@ -56,9 +105,43 @@ function MapLi({ ); } -function Map({ forLanding = false }: MapProps): React.ReactElement { +function Map({ + forLanding = false, + isSignedIn, + currentCerts +}: MapProps): React.ReactElement { const { t } = useTranslation(); + const isTracking = (stage: SuperBlocks) => + ![ + ...superBlockOrder[SuperBlockStages.Upcoming], + ...superBlockOrder[SuperBlockStages.Extra] + ].includes(stage); + + const isCompleted = (stage: SuperBlocks) => { + return isSignedIn + ? Boolean( + currentCerts?.find( + (cert: { certSlug: string }) => + (certSlugTypeMap as { [key: string]: string })[cert.certSlug] === + (superBlockCertTypeMap as { [key: string]: string })[stage] + ) + ) + : false; + }; + + const isClaimed = (stage: SuperBlocks) => { + return isSignedIn + ? Boolean( + currentCerts?.find( + (cert: { certSlug: string }) => + (certSlugTypeMap as { [key: string]: string })[cert.certSlug] === + (superBlockCertTypeMap as { [key: string]: string })[stage] + )?.show + ) + : false; + }; + return (

@@ -66,7 +149,16 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {

    {coreCurriculum.map((superBlock, i) => ( - + ))}
@@ -75,7 +167,16 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {
    {superBlockOrder[SuperBlockStages.English].map((superBlock, i) => ( - + ))}
@@ -84,7 +185,18 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {
    {superBlockOrder[SuperBlockStages.Professional].map((superBlock, i) => ( - + ))}
@@ -93,7 +205,16 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {
    {superBlockOrder[SuperBlockStages.Extra].map((superBlock, i) => ( - + ))}
@@ -102,7 +223,16 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {
    {superBlockOrder[SuperBlockStages.Legacy].map((superBlock, i) => ( - + ))}
{showUpcomingChanges && ( @@ -113,7 +243,18 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {
    {superBlockOrder[SuperBlockStages.Upcoming].map((superBlock, i) => ( - + ))}
@@ -124,4 +265,4 @@ function Map({ forLanding = false }: MapProps): React.ReactElement { Map.displayName = 'Map'; -export default Map; +export default connect(mapStateToProps)(Map); diff --git a/client/src/components/Map/map.css b/client/src/components/Map/map.css index 47b609ffdeb..360f2a4a897 100644 --- a/client/src/components/Map/map.css +++ b/client/src/components/Map/map.css @@ -8,9 +8,66 @@ padding: 0; } +.progression-arrow { + position: absolute; + bottom: -18px; + left: -18px; +} + +.map-ui ul .progress-icon { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 4rem; + margin: 0 10px 10px 0; + position: relative; +} + +.map-ui ul .progress-icon .cert-icon-outline { + width: 4rem; + height: 4rem; + display: flex; + align-items: center; + justify-content: flex-end; + background-color: var(--secondary-color); + border-radius: 50%; +} + +.map-ui ul li .progress-icon .cert-icon-outline > svg { + display: inline-block; + height: 3rem; + width: auto; +} + +.map-ui ul li { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + position: relative; +} + +.map-ui ul li .arrow path { + fill: var(--secondary-color); +} + +.map-ui ul li .progress-number { + width: calc(42px - 0.1rem); + height: 4rem; + border-radius: 50%; + background-color: var(--quaternary-background); + color: var(--secondary-color); + border: 0.2rem solid var(--secondary-color); + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; +} + @media (max-width: 640px) { .map-ui .block ul { - padding-inline-start: 6px; + padding-inline-start: 6rem; font-size: 0.9rem; } diff --git a/client/src/components/layouts/global.css b/client/src/components/layouts/global.css index d0a8bd9cdc4..1b2d47a83a9 100644 --- a/client/src/components/layouts/global.css +++ b/client/src/components/layouts/global.css @@ -372,7 +372,8 @@ fieldset[disabled] .btn-primary.focus { fill: var(--quaternary-background); } -.link-btn.btn-lg:hover .map-icon .inverted-color { +.link-btn.btn-lg:hover .map-icon .inverted-color, +.map-arrow-icon { fill: var(--secondary-color); } @@ -382,6 +383,10 @@ fieldset[disabled] .btn-primary.focus { margin-inline: 5px; } +.map-arrow-icon { + stroke: var(--secondary-color); +} + .cert-header-icon { display: block; width: 80px; @@ -568,7 +573,7 @@ blockquote .small { background-color: black; } -/* +/* * /learn sets some default styles to all `h2`s. * These rules are to override the defaults and apply danger styles to `danger` modal. */