Skip to content

Commit

Permalink
feat(learn): create a progress indicator map (freeCodeCamp#53963)
Browse files Browse the repository at this point in the history
Co-authored-by: sembauke <semboot699@gmail.com>
  • Loading branch information
2 people authored and pull[bot] committed Nov 6, 2024
1 parent fd0058a commit 0ee6ba5
Show file tree
Hide file tree
Showing 4 changed files with 313 additions and 13 deletions.
97 changes: 97 additions & 0 deletions client/src/assets/icons/completion-ribbon.tsx
Original file line number Diff line number Diff line change
@@ -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';
161 changes: 151 additions & 10 deletions client/src/components/Map/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 = {
Expand All @@ -31,19 +48,51 @@ 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 (
<>
<li
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} />
Expand All @@ -56,17 +105,60 @@ 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' : ''}>
{t('landing.core-certs-heading')}
</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' />
Expand All @@ -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' />
Expand All @@ -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' />
Expand All @@ -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' />
Expand All @@ -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 && (
Expand All @@ -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>
</>
Expand All @@ -124,4 +265,4 @@ function Map({ forLanding = false }: MapProps): React.ReactElement {

Map.displayName = 'Map';

export default Map;
export default connect(mapStateToProps)(Map);
Loading

0 comments on commit 0ee6ba5

Please sign in to comment.