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:
@ -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."
|
||||
|
@ -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",
|
||||
|
26
client/src/assets/icons/dropdown.tsx
Normal file
26
client/src/assets/icons/dropdown.tsx
Normal 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;
|
@ -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 />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 `}>
|
||||
|
@ -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')}{' '}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -217,7 +217,6 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
|
||||
)}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
{blockDashedName !== 'project-euler' ? <Spacer /> : null}
|
||||
</Fragment>
|
||||
))}
|
||||
{superBlock !== SuperBlocks.CodingInterviewPrep && (
|
||||
|
5
client/src/utils/is-new-responsive-web-design-cert.ts
Normal file
5
client/src/utils/is-new-responsive-web-design-cert.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { SuperBlocks } from '../../../config/certification-settings';
|
||||
|
||||
export default function IsNewRespCert(superBlock: string): boolean {
|
||||
return superBlock === SuperBlocks.RespWebDesignNew;
|
||||
}
|
Reference in New Issue
Block a user