feat(client): styling for project cards (#44771)

* feat: project -> certification project

* feat: add certification icon

* feat: cert project banner

* feat: combine new cert detection methods

* feat: add a dynamic tag

* feat: replace blocks with grid bloks for new RWD

* feat: adjust individual progress bar

* feat: add dropdown icon

* feat: add conditional rendering

* feat: add local and fix cert card link

Co-authored-by: ahmad abdolsaheb <ahmad.abdolsaheb@gmail.com>
This commit is contained in:
Naomi Carrigan
2022-02-08 17:49:00 -08:00
committed by GitHub
parent 95e473d4bb
commit a430aa42ef
9 changed files with 347 additions and 88 deletions

View File

@ -76,31 +76,31 @@
"note": "Note: Some browser extensions, such as ad-blockers and dark mode extensions can interfere with the tests. If you face issues, we recommend disabling extensions that modify the content or layout of pages, while taking the course.",
"blocks": {
"build-a-tribute-page-project": {
"title": "Build a Tribute Page Project",
"title": "Tribute Page",
"intro": [
"This is one of the required projects to earn your certification.",
"For this project, you will build a tribute page for a subject of your choosing, fictional or real."
] },
"build-a-personal-portfolio-webpage-project": {
"title": "Build a Personal Portfolio Webpage Project",
"title": "Personal Portfolio Webpage",
"intro": [
"This is one of the required projects to earn your certification.",
"For this project, you will build your own personal portfolio page."
] },
"build-a-product-landing-page-project": {
"title": "Build a Product Landing Page Project",
"title": "Product Landing Page",
"intro": [
"This is one of the required projects to earn your certification.",
"For this project, you will build a product landing page to market a product of your choice."
] },
"build-a-survey-form-project": {
"title": "Build a Survey Form Project",
"title": "Survey Form",
"intro": [
"This is one of the required projects to earn your certification.",
"For this project, you will build a survey form to collect data from your users."
] },
"build-a-technical-documentation-page-project": {
"title": "Build a Technical Documentation Page Project",
"title": "Technical Documentation Page",
"intro": [
"This is one of the required projects to earn your certification.",
"For this project, you will build a technical documentation page to serve as instruction or reference for a topic."

View File

@ -418,7 +418,8 @@
"email": "Email",
"and": "and",
"change-theme": "Sign in to change theme.",
"translation-pending": "Help us translate"
"translation-pending": "Help us translate",
"certification-project": "Certification Project"
},
"icons": {
"gold-cup": "Gold Cup",

View File

@ -0,0 +1,26 @@
import React from 'react';
function DropDown(): JSX.Element {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
width='10'
height='10'
viewBox='0 0 389 254'
fill='none'
>
<path
d='M194.5 0L388.5 254H307.5L194.5 99L78.5 254H0.5L194.5 0Z'
style={{
stroke: 'var(--primary-color)',
fill: 'var(--primary-color)',
strokeWidth: '1px'
}}
/>
</svg>
);
}
DropDown.displayName = 'DropDown';
export default DropDown;

View File

@ -1,22 +1,25 @@
import React, { Component } from 'react';
import { withTranslation, TFunction } from 'react-i18next';
import { ProgressBar } from '@freecodecamp/react-bootstrap';
import { connect } from 'react-redux';
import ScrollableAnchor from 'react-scrollable-anchor';
import { bindActionCreators, Dispatch } from 'redux';
import { createSelector } from 'reselect';
import { SuperBlocks } from '../../../../../config/certification-settings';
import envData from '../../../../../config/env.json';
import { isAuditedCert } from '../../../../../utils/is-audited';
import Caret from '../../../assets/icons/caret';
import DropDown from '../../../assets/icons/dropdown';
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
import GreenPass from '../../../assets/icons/green-pass';
import { Link } from '../../../components/helpers';
import { Link, Spacer } from '../../../components/helpers';
import { completedChallengesSelector, executeGA } from '../../../redux';
import { ChallengeNode, CompletedChallenge } from '../../../redux/prop-types';
import { playTone } from '../../../utils/tone';
import { makeExpandedBlockSelector, toggleBlock } from '../redux';
import IsNewRespCert from '../../../utils/is-new-responsive-web-design-cert';
import Challenges from './challenges';
import '../intro.css';
const { curriculumLocale } = envData;
@ -101,6 +104,8 @@ export class Block extends Component<BlockProps> {
t
} = this.props;
const isNewResponsiveWebDesign = IsNewRespCert(superBlock);
let completedCount = 0;
const challengesWithCompleted = challenges.map(({ challenge }) => {
const { id } = challenge;
@ -143,84 +148,191 @@ export class Block extends Component<BlockProps> {
collapse: string;
} = t('intro:misc-text');
return isProjectBlock ? (
<ScrollableAnchor id={blockDashedName}>
<div className='block'>
<div className='block-title-wrapper'>
<a className='block-link' href={`#${blockDashedName}`}>
<h3 className='big-block-title'>
{blockTitle}
<span className='block-link-icon'>#</span>
</h3>
</a>
{!isAuditedCert(curriculumLocale, superBlock) && (
<div className='block-cta-wrapper'>
<Link
className='block-title-translation-cta'
to={t('links:help-translate-link-url')}
>
{t('misc.translation-pending')}
</Link>
</div>
)}
</div>
{this.renderBlockIntros(blockIntroArr)}
<Challenges
challengesWithCompleted={challengesWithCompleted}
isProjectBlock={isProjectBlock}
superBlock={superBlock}
/>
</div>
</ScrollableAnchor>
) : (
<ScrollableAnchor id={blockDashedName}>
<div className={`block ${isExpanded ? 'open' : ''}`}>
<div className='block-title-wrapper'>
<a className='block-link' href={`#${blockDashedName}`}>
<h3 className='big-block-title'>
{blockTitle}
<span className='block-link-icon'>#</span>
</h3>
</a>
{!isAuditedCert(curriculumLocale, superBlock) && (
<div className='block-cta-wrapper'>
<Link
className='block-title-translation-cta'
to={t('links:help-translate-link-url')}
>
{t('misc.translation-pending')}
</Link>
</div>
)}
</div>
{this.renderBlockIntros(blockIntroArr)}
<button
aria-expanded={isExpanded}
className='map-title'
onClick={() => {
this.handleBlockClick();
}}
>
<Caret />
<h4 className='course-title'>
{`${isExpanded ? collapseText : expandText}`}
</h4>
<div className='map-title-completed course-title'>
{this.renderCheckMark(
completedCount === challengesWithCompleted.length
const isBlockCompleted = completedCount === challengesWithCompleted.length;
const percentageComplated = Math.floor(
(completedCount / challengesWithCompleted.length) * 100
);
const progressBarRender = (
<div className='progress-wrapper'>
<ProgressBar now={percentageComplated} />
<span>{`${percentageComplated}%`}</span>
</div>
);
const Block = (
<>
{' '}
<ScrollableAnchor id={blockDashedName}>
<div className={`block ${isExpanded ? 'open' : ''}`}>
<div className='block-header'>
<a className='block-link' href={`#${blockDashedName}`}>
<h3 className='big-block-title'>
{blockTitle}
<span className='block-link-icon'>#</span>
</h3>
</a>
{!isAuditedCert(curriculumLocale, superBlock) && (
<div className='block-cta-wrapper'>
<Link
className='block-title-translation-cta'
to={t('links:help-translate-link-url')}
>
{t('misc.translation-pending')}
</Link>
</div>
)}
<span className='map-completed-count'>{`${completedCount}/${challengesWithCompleted.length}`}</span>
</div>
</button>
{isExpanded && (
{this.renderBlockIntros(blockIntroArr)}
<button
aria-expanded={isExpanded}
className='map-title'
onClick={() => {
this.handleBlockClick();
}}
>
<Caret />
<h4 className='course-title'>
{`${isExpanded ? collapseText : expandText}`}
</h4>
<div className='map-title-completed course-title'>
{this.renderCheckMark(isBlockCompleted)}
<span className='map-completed-count'>{`${completedCount}/${challengesWithCompleted.length}`}</span>
</div>
</button>
{isExpanded && (
<Challenges
challengesWithCompleted={challengesWithCompleted}
isProjectBlock={isProjectBlock}
superBlock={superBlock}
/>
)}
</div>
</ScrollableAnchor>
</>
);
const ProjectBlock = (
<>
<ScrollableAnchor id={blockDashedName}>
<div className='block'>
<div className='block-header'>
<a className='block-link' href={`#${blockDashedName}`}>
<h3 className='big-block-title'>
{blockTitle}
<span className='block-link-icon'>#</span>
</h3>
</a>
{!isAuditedCert(curriculumLocale, superBlock) && (
<div className='block-cta-wrapper'>
<Link
className='block-title-translation-cta'
to={t('links:help-translate-link-url')}
>
{t('misc.translation-pending')}
</Link>
</div>
)}
</div>
{this.renderBlockIntros(blockIntroArr)}
<Challenges
challengesWithCompleted={challengesWithCompleted}
isProjectBlock={isProjectBlock}
superBlock={superBlock}
/>
)}
</div>
</ScrollableAnchor>
</div>
</ScrollableAnchor>
</>
);
const GridBlock = (
<>
{' '}
<ScrollableAnchor id={blockDashedName}>
<div className={`block block-grid ${isExpanded ? 'open' : ''}`}>
<a
className='block-header'
onClick={() => {
this.handleBlockClick();
}}
href={`#${blockDashedName}`}
>
<div className='tags-wrapper'>
{!isAuditedCert(curriculumLocale, superBlock) && (
<Link
className='cert-tag'
to={t('links:help-translate-link-url')}
>
{t('misc.translation-pending')}
</Link>
)}
</div>
<div className='title-wrapper map-title'>
{this.renderCheckMark(isBlockCompleted)}
<h3 className='block-grid-title'>{blockTitle}</h3>
<DropDown />
</div>
{isExpanded && this.renderBlockIntros(blockIntroArr)}
{!isExpanded &&
!isBlockCompleted &&
completedCount > 0 &&
progressBarRender}
</a>
{isExpanded && (
<>
<Challenges
challengesWithCompleted={challengesWithCompleted}
isProjectBlock={isProjectBlock}
superBlock={superBlock}
/>
</>
)}
</div>
</ScrollableAnchor>
</>
);
const GridProjectBlock = (
<div className='block block-grid grid-project-block'>
<a
className='block-header'
onClick={() => {
this.handleBlockClick();
}}
href={challengesWithCompleted[0].fields.slug}
>
<div className='tags-wrapper'>
<span className='cert-tag'>{t('misc.certification-project')}</span>
{!isAuditedCert(curriculumLocale, superBlock) && (
<Link
className='cert-tag'
to={t('links:help-translate-link-url')}
>
{t('misc.translation-pending')}
</Link>
)}
</div>
<div className='title-wrapper map-title'>
{this.renderCheckMark(isBlockCompleted)}
<h3 className='block-grid-title'>{blockTitle}</h3>
</div>
{this.renderBlockIntros(blockIntroArr)}
</a>
</div>
);
const blockrenderer = () => {
if (isProjectBlock)
return isNewResponsiveWebDesign ? GridProjectBlock : ProjectBlock;
return isNewResponsiveWebDesign ? GridBlock : Block;
};
return (
<>
{blockrenderer()}
{isNewResponsiveWebDesign && !isProjectBlock ? null : <Spacer />}
</>
);
}
}

View File

@ -11,6 +11,7 @@ import { executeGA } from '../../../redux';
import { SuperBlocks } from '../../../../../config/certification-settings';
import { ExecuteGaArg } from '../../../pages/donate';
import { ChallengeWithCompletedNode } from '../../../redux/prop-types';
import isNewRespCert from '../../../utils/is-new-responsive-web-design-cert';
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators({ executeGA }, dispatch);
@ -46,7 +47,7 @@ function Challenges({
<GreenNotCompleted style={mapIconStyle} />
);
const isGridMap = superBlock === SuperBlocks.RespWebDesignNew;
const isGridMap = isNewRespCert(superBlock);
return isGridMap ? (
<ul className={`map-challenges-ul map-challenges-grid `}>

View File

@ -2,6 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { Alert } from '@freecodecamp/react-bootstrap';
import { SuperBlocks } from '../../../../../config/certification-settings';
import IsNewRespCert from '../../../utils/is-new-responsive-web-design-cert';
import { Link } from '../../../components/helpers';
interface LegacyLinksProps {
@ -12,7 +13,7 @@ function LegacyLinks({ superBlock }: LegacyLinksProps): JSX.Element {
const { t } = useTranslation();
return (
<>
{superBlock === SuperBlocks.RespWebDesignNew && (
{IsNewRespCert(superBlock) && (
<Alert bsStyle='info'>
<p>
{t('intro:misc-text.viewing-upcoming-change')}{' '}

View File

@ -13,15 +13,16 @@
overflow-wrap: break-word;
}
.block-title-wrapper {
.block-header {
display: flex;
justify-content: space-between;
flex-direction: row;
}
.block-title-wrapper .block-link {
.block-header .block-link {
flex-grow: 3;
flex-basis: 0;
padding: 25px 15px 10px;
}
.block-link:hover,
@ -30,6 +31,13 @@
background-color: var(--primary-background);
}
a.cert-tag:hover,
a.cert-tag:focus,
a.cert-tag:active {
color: var(--highlight-background);
background-color: var(--highlight-color);
}
.block-link {
cursor: pointer;
}
@ -48,6 +56,12 @@
overflow-wrap: break-word;
}
.block-grid-title {
font-size: 1.2rem;
margin: 0;
overflow-wrap: break-word;
}
.block-title-translation-cta {
white-space: nowrap;
padding: 0.2em 0.5em;
@ -162,9 +176,6 @@ button.map-title {
background: var(--primary-background);
}
.block-ui .block .big-block-title {
padding: 25px 15px 10px;
}
.block-ui .block .block-description {
padding: 0 15px 15px;
border-bottom: 3px solid var(--secondary-background);
@ -212,6 +223,14 @@ button.map-title {
transform: rotate(90deg);
}
.block-grid .map-title > svg:last-child {
transform: rotate(180deg);
}
.block-grid.open .map-title > svg:last-child {
transform: rotate(0deg);
}
.map-challenges-ul {
padding-inline-start: 0;
margin-bottom: 0;
@ -269,7 +288,7 @@ button.map-title {
font-size: 1.17rem;
}
.block-title-wrapper {
.block-header {
flex-direction: column;
}
@ -325,3 +344,98 @@ button.map-title {
text-decoration-style: none;
color: var(--secondary-color);
}
.cert-tag {
text-align: left;
width: fit-content;
font-size: 1rem;
display: block;
margin-bottom: 5px;
margin-right: 5px;
padding: 4px 10px;
color: var(--highlight-color);
background-color: var(--highlight-background);
}
.block-grid {
border-bottom: 3px solid var(--secondary-background);
}
.block-grid .block-header {
flex-direction: column;
}
.block-grid .block-header {
display: flex;
background: transparent;
border: none;
text-align: left;
width: 100%;
padding: 0;
cursor: pointer;
padding: 18px 15px;
}
.block-grid .block-header:hover {
color: var(--tertiary-color);
background-color: var(--tertiary-background);
}
.block-grid .block-header .block-link {
flex-direction: row;
}
.block-ui .block-grid .block-description {
border: none;
padding: 0 10px 10px;
}
.block-grid .map-title > svg:last-child {
margin-left: auto;
}
.block-grid .map-title > svg {
margin: 10px;
}
.title-wrapper {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}
.block-grid .progress-wrapper {
width: 100%;
text-align: left;
display: flex;
align-items: center;
margin-top: 18px;
margin-bottom: 14px;
}
.block-grid .progress-wrapper span {
color: var(--quaternary-color);
}
.block-grid .progress {
height: 15px;
background-color: var(--secondary-background);
margin: 0 10px;
width: 80%;
border-radius: 0;
}
.block-grid .progress-bar {
background-color: var(--blue-mid);
}
.tags-wrapper {
display: flex;
width: 100%;
padding: 10px 10px 0px 10px;
}
.grid-project-block {
margin-bottom: 50px;
}

View File

@ -217,7 +217,6 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
)}
superBlock={superBlock}
/>
{blockDashedName !== 'project-euler' ? <Spacer /> : null}
</Fragment>
))}
{superBlock !== SuperBlocks.CodingInterviewPrep && (

View File

@ -0,0 +1,5 @@
import { SuperBlocks } from '../../../config/certification-settings';
export default function IsNewRespCert(superBlock: string): boolean {
return superBlock === SuperBlocks.RespWebDesignNew;
}