mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-01-06 06:01:31 -05:00
feat(client): display FSD in multi-level accordion (#56941)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -1704,6 +1704,73 @@
|
||||
"Through a blend of interactive lessons, coding exercises, and real-world projects, you will master both frontend and backend development. You'll work with HTML, CSS, and JavaScript to build responsive user interfaces, explore React and TypeScript for advanced web applications, and learn to manage data with relational databases - and on the backend, you'll use Git, Npm, Node.js, and Python to create powerful server-side solutions.",
|
||||
"By the end of this course, you'll have the practical skills and experience to confidently develop complete web applications, preparing you for a successful career as a Full Stack Developer."
|
||||
],
|
||||
"chapters": {
|
||||
"freecodecamp": "freeCodeCamp",
|
||||
"html": "HTML",
|
||||
"css": "CSS",
|
||||
"javascript": "JavaScript",
|
||||
"frontend-libraries": "Front End Libraries",
|
||||
"relational-databases": "Relational Databases",
|
||||
"security-and-privacy": "Security and Privacy"
|
||||
},
|
||||
"modules": {
|
||||
"getting-started-with-freecodecamp": "Getting Started with freeCodeCamp",
|
||||
"basic-html": "Basic HTML",
|
||||
"semantic-html": "Semantic HTML",
|
||||
"html-forms-and-tables": "HTML Forms and Tables",
|
||||
"html-and-accessibility": "HTML and Accessibility",
|
||||
"computer-basics": "Computer Basics",
|
||||
"basic-css": "Basic CSS",
|
||||
"design-for-developers": "Designer for Developers",
|
||||
"absolute-and-relative-units": "Absolute and Relative Units",
|
||||
"pseudo-classes-and-elements": "Pseudo Classes and Elements",
|
||||
"css-colors": "CSS Colors",
|
||||
"styling-forms": "Styling Forms",
|
||||
"css-box-model": "CSS Box Model",
|
||||
"css-flexbox": "CSS Flexbox",
|
||||
"css-typography": "CSS Typography",
|
||||
"css-and-accessibility": "CSS and Accessibility",
|
||||
"attribute-selectors": "Attribute Selectors",
|
||||
"css-positioning": "CSS Positioning",
|
||||
"responsive-design": "Responsive Design",
|
||||
"css-variables": "CSS Variables",
|
||||
"css-grid": "CSS Grid",
|
||||
"css-animations": "CSS Animations",
|
||||
"code-editors": "Code editors",
|
||||
"javascript-variables-and-strings": "JavaScript Variables and Strings",
|
||||
"javascript-booleans-and-numbers": "JavaScript Booleans and Numbers",
|
||||
"javascript-functions": "JavaScript Functions",
|
||||
"javascript-arrays": "JavaScript Arrays",
|
||||
"javascript-objects": "JavaScript Objects",
|
||||
"javascript-loops": "JavaScript Loops",
|
||||
"review-javascript-fundamentals": "Review JavaScript Fundamentals",
|
||||
"higher-order-functions-and-callbacks": "Higher Order Functions and Callbacks",
|
||||
"dom-manipulation-and-events": "DOM Manipulation and Events",
|
||||
"debugging-javascript": "Debugging JavaScript",
|
||||
"basic-regex": "Basic Regex",
|
||||
"form-validation": "Form Validation",
|
||||
"javascript-dates": "JavaScript Dates",
|
||||
"audio-and-video-events": "Audio and Video Events",
|
||||
"maps-sets-and-json": "Maps, Sets, and JSON",
|
||||
"localstorage-and-crud-operations": "localStorage and CRUD Operations",
|
||||
"classes-and-the-this-keyword": "Classes and the <code>this</code> keyword",
|
||||
"recursion": "Recursion",
|
||||
"functional-programming": "Functional Programming",
|
||||
"asynchronous-javascript": "Asynchronous JavaScript",
|
||||
"react-fundamentals": "React Fundamentals",
|
||||
"react-state-hooks-and-routing": "React state, hooks, and routing",
|
||||
"performance": "Performance",
|
||||
"css-libraries-and-frameworks": "CSS Libraries and Frameworks",
|
||||
"testing": "Testing",
|
||||
"typescript-fundamentals": "TypeScript Fundamentals",
|
||||
"bash-fundamentals": "Bash Fundamentals",
|
||||
"relational-databases": "Relational Databases",
|
||||
"bash-scripting": "Bash Scripting",
|
||||
"sql-and-bash": "SQL and Bash",
|
||||
"nano": "Nano",
|
||||
"git": "Git",
|
||||
"security-and-privacy": "Security and Privacy"
|
||||
},
|
||||
"blocks": {
|
||||
"lecture-welcome-to-freecodecamp": {
|
||||
"title": "Welcome to freeCodeCamp",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"@freecodecamp/react-calendar-heatmap": "1.1.0",
|
||||
"@freecodecamp/ui": "3.1.0",
|
||||
"@growthbook/growthbook-react": "0.20.0",
|
||||
"@headlessui/react": "1.7.19",
|
||||
"@loadable/component": "5.16.3",
|
||||
"@reach/router": "1.3.4",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
|
||||
@@ -9,6 +9,7 @@ function DropDown(): JSX.Element {
|
||||
height='10'
|
||||
viewBox='0 0 389 254'
|
||||
fill='none'
|
||||
className='dropdown-icon'
|
||||
>
|
||||
<path
|
||||
d='M194.5 0L388.5 254H307.5L194.5 99L78.5 254H0.5L194.5 0Z'
|
||||
|
||||
@@ -228,6 +228,8 @@ export type ChallengeNode = {
|
||||
videoLocaleIds?: VideoLocaleIds;
|
||||
bilibiliIds?: BilibiliIds;
|
||||
videoUrl: string;
|
||||
chapter?: string;
|
||||
module?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
.super-block-accordion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.super-block-accordion ul {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.super-block-accordion li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.super-block-accordion li a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter,
|
||||
.super-block-accordion .exam {
|
||||
background-color: var(--primary-background);
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-button {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.super-block-accordion .module-button {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-button:hover,
|
||||
.super-block-accordion .module-button:hover {
|
||||
background-color: var(--tertiary-background);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-button[aria-expanded='false'] .dropdown-icon,
|
||||
.super-block-accordion .module-button[aria-expanded='false'] .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-button[aria-expanded='true'] .dropdown-icon,
|
||||
.super-block-accordion .module-button[aria-expanded='true'] .dropdown-icon {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-panel {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.super-block-accordion .module-panel {
|
||||
padding: 0 24px;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import React, { ReactNode, useMemo } from 'react';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
// TODO: Add this component to freecodecamp/ui and remove this dependency
|
||||
import { Disclosure } from '@headlessui/react';
|
||||
|
||||
import { ChallengeNode } from '../../../redux/prop-types';
|
||||
import { SuperBlocks } from '../../../../../shared/config/curriculum';
|
||||
import DropDown from '../../../assets/icons/dropdown';
|
||||
import { BlockTypes } from '../../../../../shared/config/blocks';
|
||||
import Block from './block';
|
||||
|
||||
import './super-block-accordion.css';
|
||||
|
||||
interface ChapterProps {
|
||||
dashedName: string;
|
||||
children: ReactNode;
|
||||
isExpanded: boolean;
|
||||
}
|
||||
|
||||
interface ModuleProps {
|
||||
dashedName: string;
|
||||
children: ReactNode;
|
||||
isExpanded: boolean;
|
||||
}
|
||||
interface SuperBlockTreeViewProps {
|
||||
challenges: ChallengeNode['challenge'][];
|
||||
superBlock: SuperBlocks;
|
||||
chosenBlock: string;
|
||||
}
|
||||
|
||||
const Chapter = ({ dashedName, children, isExpanded }: ChapterProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Disclosure as='li' className='chapter' defaultOpen={isExpanded}>
|
||||
<Disclosure.Button className='chapter-button'>
|
||||
{t(`intro:full-stack-developer.chapters.${dashedName}`)}
|
||||
<DropDown />
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as='ul' className='chapter-panel'>
|
||||
{children}
|
||||
</Disclosure.Panel>
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
|
||||
const Module = ({ dashedName, children, isExpanded }: ModuleProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Disclosure as='li' defaultOpen={isExpanded}>
|
||||
<Disclosure.Button className='module-button'>
|
||||
<DropDown />
|
||||
{t(`intro:full-stack-developer.modules.${dashedName}`)}
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as='ul' className='module-panel'>
|
||||
{children}
|
||||
</Disclosure.Panel>
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
|
||||
export const SuperBlockAccordion = ({
|
||||
challenges,
|
||||
superBlock,
|
||||
chosenBlock
|
||||
}: SuperBlockTreeViewProps) => {
|
||||
const { allChapters, allBlocks, examChallenge } = useMemo(() => {
|
||||
const allBlocks = uniqBy(challenges, 'block').map(
|
||||
({ block, blockType, chapter, module }) => ({
|
||||
name: block,
|
||||
blockType,
|
||||
chapter: chapter as string,
|
||||
module: module as string,
|
||||
challenges: challenges.filter(({ block: b }) => b === block)
|
||||
})
|
||||
);
|
||||
|
||||
const allModules = uniqBy(allBlocks, 'module').map(
|
||||
({ module, chapter }) => ({
|
||||
name: module,
|
||||
chapter,
|
||||
blocks: allBlocks.filter(({ module: m }) => m === module)
|
||||
})
|
||||
);
|
||||
|
||||
const allChapters = uniqBy(allModules, 'chapter').map(({ chapter }) => ({
|
||||
name: chapter,
|
||||
modules: allModules.filter(({ chapter: c }) => c === chapter)
|
||||
}));
|
||||
|
||||
const examChallenge = challenges.find(
|
||||
({ blockType }) => blockType === BlockTypes.exam
|
||||
);
|
||||
|
||||
return { allChapters, allModules, allBlocks, examChallenge };
|
||||
}, [challenges]);
|
||||
|
||||
// Expand the outer layers in order to reveal the chosen block.
|
||||
const expandedChapter = allBlocks.find(
|
||||
({ name }) => chosenBlock === name
|
||||
)?.chapter;
|
||||
const expandedModule = allBlocks.find(
|
||||
({ name }) => chosenBlock === name
|
||||
)?.module;
|
||||
|
||||
return (
|
||||
<ul className='super-block-accordion'>
|
||||
{allChapters.map(chapter => {
|
||||
if (examChallenge && chapter.name === examChallenge.chapter) {
|
||||
return (
|
||||
<li key={examChallenge.dashedName} className='exam'>
|
||||
<Block
|
||||
block={examChallenge.block}
|
||||
blockType={examChallenge.blockType}
|
||||
challenges={[examChallenge]}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Chapter
|
||||
key={chapter.name}
|
||||
dashedName={chapter.name}
|
||||
isExpanded={expandedChapter === chapter.name}
|
||||
>
|
||||
{chapter.modules.map(mod => (
|
||||
<Module
|
||||
key={mod.name}
|
||||
dashedName={mod.name}
|
||||
isExpanded={expandedModule === mod.name}
|
||||
>
|
||||
{mod.blocks.map(block => (
|
||||
<li key={block.name}>
|
||||
<Block
|
||||
block={block.name}
|
||||
blockType={block.blockType}
|
||||
challenges={block.challenges}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</Module>
|
||||
))}
|
||||
</Chapter>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
@@ -599,21 +599,15 @@ a.map-grid-item.challenge-completed {
|
||||
background: var(--primary-background);
|
||||
}
|
||||
|
||||
/* Override the `.block-description` padding (25px)
|
||||
to align the description with the content in the panel. */
|
||||
.block-ui .challenge-list-block .block-header .block-description,
|
||||
.block-ui .challenge-grid-block .block-header .block-description {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.block-ui .challenge-grid-block .progress-wrapper {
|
||||
/* Make the progress bar align with the description above */
|
||||
padding-inline-start: 10px;
|
||||
}
|
||||
|
||||
.block-ui .challenge-grid-block .challenge-grid-block-panel {
|
||||
.challenge-grid-block .challenge-grid-block-panel {
|
||||
/* Add some space between panel content and the top edge of the container.
|
||||
The value (18px) is the same value `.map-challenges-grid` uses
|
||||
to create space between the grid and the bottom edge of the container. */
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.chapter-button .dropdown-icon,
|
||||
.module-button .dropdown-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WindowLocation } from '@reach/router';
|
||||
import { graphql } from 'gatsby';
|
||||
import { uniq } from 'lodash-es';
|
||||
import React, { Fragment, useEffect, memo } from 'react';
|
||||
import React, { Fragment, useEffect, memo, useMemo } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { useTranslation, withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -31,6 +31,7 @@ import CertChallenge from './components/cert-challenge';
|
||||
import LegacyLinks from './components/legacy-links';
|
||||
import HelpTranslate from './components/help-translate';
|
||||
import SuperBlockIntro from './components/super-block-intro';
|
||||
import { SuperBlockAccordion } from './components/super-block-accordion';
|
||||
import { resetExpansion, toggleBlock } from './redux';
|
||||
|
||||
import './intro.css';
|
||||
@@ -174,8 +175,14 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
|
||||
pageContext: { superBlock, title, certification }
|
||||
} = props;
|
||||
|
||||
const allChallenges = nodes.map(({ challenge }) => challenge);
|
||||
const challenges = allChallenges.filter(c => c.superBlock === superBlock);
|
||||
const allChallenges = useMemo(
|
||||
() => nodes.map(({ challenge }) => challenge),
|
||||
[nodes]
|
||||
);
|
||||
const challenges = useMemo(
|
||||
() => allChallenges.filter(c => c.superBlock === superBlock),
|
||||
[allChallenges, superBlock]
|
||||
);
|
||||
const blocks = uniq(challenges.map(({ block }) => block));
|
||||
|
||||
const i18nTitle = getSuperBlockTitleForMap(superBlock);
|
||||
@@ -190,6 +197,9 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
|
||||
SuperBlocks.PythonForEverybody
|
||||
];
|
||||
|
||||
const superBlockWithAccordionView = [SuperBlocks.FullStackDeveloper];
|
||||
const chosenBlock = getChosenBlock();
|
||||
|
||||
const onCertificationDonationAlertClick = () => {
|
||||
callGA({
|
||||
event: 'donation_related',
|
||||
@@ -221,32 +231,40 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
|
||||
{t(`intro:misc-text.courses`)}
|
||||
</h2>
|
||||
<Spacer size='m' />
|
||||
<div className='block-ui'>
|
||||
{blocks.map(block => {
|
||||
const blockChallenges = challenges.filter(
|
||||
c => c.block === block
|
||||
);
|
||||
const blockType = blockChallenges[0].blockType;
|
||||
{superBlockWithAccordionView.includes(superBlock) ? (
|
||||
<SuperBlockAccordion
|
||||
challenges={challenges}
|
||||
superBlock={superBlock}
|
||||
chosenBlock={chosenBlock}
|
||||
/>
|
||||
) : (
|
||||
<div className='block-ui'>
|
||||
{blocks.map(block => {
|
||||
const blockChallenges = challenges.filter(
|
||||
c => c.block === block
|
||||
);
|
||||
const blockType = blockChallenges[0].blockType;
|
||||
|
||||
return (
|
||||
<Block
|
||||
key={block}
|
||||
block={block}
|
||||
blockType={blockType}
|
||||
challenges={blockChallenges}
|
||||
return (
|
||||
<Block
|
||||
key={block}
|
||||
block={block}
|
||||
blockType={blockType}
|
||||
challenges={blockChallenges}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!superblockWithoutCert.includes(superBlock) && (
|
||||
<CertChallenge
|
||||
certification={certification}
|
||||
superBlock={superBlock}
|
||||
title={title}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!superblockWithoutCert.includes(superBlock) && (
|
||||
<CertChallenge
|
||||
certification={certification}
|
||||
superBlock={superBlock}
|
||||
title={title}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isSignedIn && !signInLoading && (
|
||||
<>
|
||||
<Spacer size='l' />
|
||||
@@ -305,6 +323,8 @@ export const query = graphql`
|
||||
superBlock
|
||||
dashedName
|
||||
blockLayout
|
||||
chapter
|
||||
module
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -476,6 +476,9 @@ importers:
|
||||
'@growthbook/growthbook-react':
|
||||
specifier: 0.20.0
|
||||
version: 0.20.0(react@16.14.0)
|
||||
'@headlessui/react':
|
||||
specifier: 1.7.19
|
||||
version: 1.7.19(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
|
||||
'@loadable/component':
|
||||
specifier: 5.16.3
|
||||
version: 5.16.3(react@16.14.0)
|
||||
|
||||
Reference in New Issue
Block a user