feat(learn): create a progress indicator map (#53963)

Co-authored-by: sembauke <semboot699@gmail.com>
This commit is contained in:
Anna
2024-04-29 12:55:24 -04:00
committed by GitHub
parent 01d6fcf64f
commit 9f7ecffc0d
4 changed files with 313 additions and 13 deletions

View File

@@ -0,0 +1,97 @@
import React from 'react';
interface RibbonProps {
value: number;
isClaimed: boolean;
isCompleted: boolean;
}
export const Arrow = () => (
<svg width={70} height={40} xmlns='http://www.w3.org/2000/svg'>
<line
x1={50}
y1={0}
x2={50}
y2={35}
stroke='black'
strokeWidth={3}
strokeDasharray='6,1.3'
className='map-arrow-icon'
/>
<rect
x={36}
y={35}
width={15}
height={2}
fill='black'
transform='rotate(45, 50, 36)'
className='map-arrow-icon'
/>
<rect
x={49}
y={35}
width={15}
height={2}
fill='black'
transform='rotate(-45, 50, 36)'
className='map-arrow-icon'
/>
</svg>
);
export const RibbonIcon = ({
value,
isCompleted: completed,
isClaimed
}: RibbonProps): JSX.Element => {
const properClassName = completed ? 'completeIcon' : 'incompleteIcon';
const fillColor = completed ? 'black' : 'gray';
return (
<svg
xmlns='http://www.w3.org/2000/svg'
width='75%'
height='75%'
viewBox='0 0 45 50'
fill='none'
className={properClassName}
aria-hidden='true'
>
{isClaimed && (
<>
<path
d='M25 35.3418L35.4851 28L44.5957 41.0113L36.2658 39.7151L34.1106 48.353L25 35.3418Z'
className='map-icon'
/>
<path
d='M9.11059 29L19.5957 36.3418L10.4851 49.353L8.85418 41.0821L-4.67677e-07 42.0113L9.11059 29Z'
className='map-icon'
/>
</>
)}
<circle cx={21.9999} cy={21} r={20} fill={fillColor} />
<circle
cx={22.5}
cy={21}
r={17.5}
fill={fillColor}
stroke='white'
strokeWidth={2}
/>
<text
x='50%'
y='50%'
fontFamily='Verdana'
color={fillColor}
fontSize='1.0rem'
fill='#fff'
textAnchor='middle'
alignmentBaseline='central'
>
{value}
</text>
</svg>
);
};
RibbonIcon.displayName = 'RibbonIcon';

View File

@@ -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 && (
<>
<div className='progress-icon'>
<RibbonIcon
value={index + 1}
isCompleted={completed}
isClaimed={claimed}
/>
</div>
<div className='progression-arrow'>{!last && <Arrow />}</div>
</>
)}
<Link className='btn link-btn btn-lg' to={`/learn/${superBlock}/`}>
<div style={linkSpacingStyle}>
<SuperBlockIcon className='map-icon' superBlock={superBlock} />
@@ -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 (
<div className='map-ui' data-test-label='curriculum-map'>
<h2 className={forLanding ? 'big-heading' : ''}>
@@ -66,7 +149,16 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {
</h2>
<ul>
{coreCurriculum.map((superBlock, i) => (
<MapLi key={i} superBlock={superBlock} landing={forLanding} />
<MapLi
key={i}
superBlock={superBlock}
landing={forLanding}
trackProgress={isTracking(superBlock)}
index={i}
claimed={isClaimed(superBlock)}
completed={isCompleted(superBlock)}
last={i + 1 == coreCurriculum.length}
/>
))}
</ul>
<Spacer size='medium' />
@@ -75,7 +167,16 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {
</h2>
<ul>
{superBlockOrder[SuperBlockStages.English].map((superBlock, i) => (
<MapLi key={i} superBlock={superBlock} landing={forLanding} />
<MapLi
key={i}
superBlock={superBlock}
landing={forLanding}
trackProgress={isTracking(superBlock)}
completed={isCompleted(superBlock)}
claimed={isClaimed(superBlock)}
index={i}
last={i + 1 == superBlockOrder[SuperBlockStages.English].length}
/>
))}
</ul>
<Spacer size='medium' />
@@ -84,7 +185,18 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {
</h2>
<ul>
{superBlockOrder[SuperBlockStages.Professional].map((superBlock, i) => (
<MapLi key={i} superBlock={superBlock} landing={forLanding} />
<MapLi
key={i}
superBlock={superBlock}
landing={forLanding}
trackProgress={isTracking(superBlock)}
completed={isCompleted(superBlock)}
claimed={isClaimed(superBlock)}
index={i}
last={
i + 1 == superBlockOrder[SuperBlockStages.Professional].length
}
/>
))}
</ul>
<Spacer size='medium' />
@@ -93,7 +205,16 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {
</h2>
<ul>
{superBlockOrder[SuperBlockStages.Extra].map((superBlock, i) => (
<MapLi key={i} superBlock={superBlock} landing={forLanding} />
<MapLi
key={i}
superBlock={superBlock}
landing={forLanding}
trackProgress={isTracking(superBlock)}
completed={isCompleted(superBlock)}
claimed={isClaimed(superBlock)}
index={i}
last={i + 1 == superBlockOrder[SuperBlockStages.Extra].length}
/>
))}
</ul>
<Spacer size='medium' />
@@ -102,7 +223,16 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {
</h2>
<ul>
{superBlockOrder[SuperBlockStages.Legacy].map((superBlock, i) => (
<MapLi key={i} superBlock={superBlock} landing={forLanding} />
<MapLi
key={i}
superBlock={superBlock}
landing={forLanding}
trackProgress={isTracking(superBlock)}
completed={isCompleted(superBlock)}
claimed={isClaimed(superBlock)}
index={i}
last={i + 1 == superBlockOrder[SuperBlockStages.Legacy].length}
/>
))}
</ul>
{showUpcomingChanges && (
@@ -113,7 +243,18 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {
</h2>
<ul>
{superBlockOrder[SuperBlockStages.Upcoming].map((superBlock, i) => (
<MapLi key={i} superBlock={superBlock} landing={forLanding} />
<MapLi
key={i}
superBlock={superBlock}
landing={forLanding}
trackProgress={isTracking(superBlock)}
completed={isCompleted(superBlock)}
index={i}
claimed={isClaimed(superBlock)}
last={
i + 1 == superBlockOrder[SuperBlockStages.Upcoming].length
}
/>
))}
</ul>
</>
@@ -124,4 +265,4 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {
Map.displayName = 'Map';
export default Map;
export default connect(mapStateToProps)(Map);

View File

@@ -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;
}

View File

@@ -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.
*/