mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-02-21 11:04:47 -05:00
feat(client): coming soon chapters, modules and blocks (#57462)
Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
3418f3dad5
commit
996d76ef20
@@ -1704,7 +1704,8 @@
|
||||
"javascript": "JavaScript",
|
||||
"frontend-libraries": "Front End Libraries",
|
||||
"relational-databases": "Relational Databases",
|
||||
"security-and-privacy": "Security and Privacy"
|
||||
"backend-javascript": "Backend JavaScript",
|
||||
"python": "Python"
|
||||
},
|
||||
"modules": {
|
||||
"getting-started-with-freecodecamp": "Getting Started with freeCodeCamp",
|
||||
|
||||
@@ -749,6 +749,7 @@
|
||||
"result-list": "Search results"
|
||||
},
|
||||
"misc": {
|
||||
"coming-soon": "Coming Soon",
|
||||
"offline": "You appear to be offline, your progress may not be saved",
|
||||
"server-offline": "The server could not be reached and your progress may not be saved. Please contact <0>support</0> if this message persists",
|
||||
"unsubscribed": "You have successfully been unsubscribed",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import type { DefaultTFuncReturn, TFunction } from 'i18next';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -72,7 +72,7 @@ class Block extends Component<BlockProps> {
|
||||
toggleBlock(block);
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
render(): ReactNode {
|
||||
const {
|
||||
block,
|
||||
blockType,
|
||||
@@ -129,6 +129,13 @@ class Block extends Component<BlockProps> {
|
||||
(completedCount / extendedChallenges.length) * 100
|
||||
);
|
||||
|
||||
// since the Blocks are not components, we need link to exist even if it's
|
||||
// not being used to render anything
|
||||
const link = challenges[0]?.fields.slug || '';
|
||||
const blockLayout = challenges[0]?.blockLayout;
|
||||
|
||||
const isEmptyBlock = !challenges.length;
|
||||
|
||||
const courseCompletionStatus = () => {
|
||||
if (completedCount === 0) {
|
||||
return t('learn.not-started');
|
||||
@@ -306,7 +313,7 @@ class Block extends Component<BlockProps> {
|
||||
onClick={() => {
|
||||
this.handleBlockClick();
|
||||
}}
|
||||
to={extendedChallenges[0].fields.slug}
|
||||
to={link}
|
||||
>
|
||||
<CheckMark isCompleted={isBlockCompleted} />
|
||||
{blockTitle}{' '}
|
||||
@@ -391,7 +398,7 @@ class Block extends Component<BlockProps> {
|
||||
onClick={() => {
|
||||
this.handleBlockClick();
|
||||
}}
|
||||
to={extendedChallenges[0].fields.slug}
|
||||
to={link}
|
||||
>
|
||||
<CheckMark isCompleted={isBlockCompleted} />
|
||||
{blockType && <BlockLabel blockType={blockType} />}
|
||||
@@ -460,11 +467,15 @@ class Block extends Component<BlockProps> {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{layoutToComponent[challenges[0].blockLayout]}
|
||||
{(!isGridBlock || isProjectBlock) &&
|
||||
superBlock !== SuperBlocks.FullStackDeveloper && <Spacer size='m' />}
|
||||
</>
|
||||
!isEmptyBlock && (
|
||||
<>
|
||||
{layoutToComponent[blockLayout]}
|
||||
{(!isGridBlock || isProjectBlock) &&
|
||||
superBlock !== SuperBlocks.FullStackDeveloper && (
|
||||
<Spacer size='m' />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter,
|
||||
.super-block-accordion .link-chapter,
|
||||
.super-block-accordion .link-module {
|
||||
.super-block-accordion .link-block {
|
||||
background-color: var(--primary-background);
|
||||
}
|
||||
|
||||
@@ -75,6 +74,40 @@
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-panel > li:last-child .block-grid {
|
||||
.super-block-accordion .chapter-panel > li {
|
||||
border-bottom: 4px solid var(--secondary-background);
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-panel > li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-panel > li:last-child .block-grid,
|
||||
.super-block-accordion .module-panel > li:last-child .block-grid {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.super-block-accordion .badge {
|
||||
color: var(--quaternary-color);
|
||||
border: 1px solid var(--quaternary-color);
|
||||
align-self: center;
|
||||
height: fit-content;
|
||||
padding: 1px 6px 2px;
|
||||
font-size: 0.85rem;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.super-block-accordion .coming-soon {
|
||||
background-color: var(--primary-background);
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 20px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.super-block-accordion .coming-soon-module {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
@@ -12,10 +11,13 @@ import DropDown from '../../../assets/icons/dropdown';
|
||||
import superBlockStructure from '../../../../../curriculum/superblock-structure/full-stack.json';
|
||||
import { ChapterIcon } from '../../../assets/chapter-icon';
|
||||
import { FsdChapters } from '../../../../../shared/config/chapters';
|
||||
import envData from '../../../../config/env.json';
|
||||
import Block from './block';
|
||||
|
||||
import './super-block-accordion.css';
|
||||
|
||||
const { showUpcomingChanges } = envData;
|
||||
|
||||
interface ChapterProps {
|
||||
dashedName: string;
|
||||
children: ReactNode;
|
||||
@@ -33,9 +35,16 @@ interface SuperBlockTreeViewProps {
|
||||
chosenBlock: string;
|
||||
}
|
||||
|
||||
const modules = superBlockStructure.chapters.flatMap(
|
||||
chapter => chapter.modules
|
||||
);
|
||||
type Module = {
|
||||
dashedName: string;
|
||||
comingSoon?: boolean;
|
||||
blocks: {
|
||||
dashedName: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const modules = superBlockStructure.chapters.flatMap(({ modules }) => modules);
|
||||
const chapters = superBlockStructure.chapters;
|
||||
|
||||
const isLinkModule = (name: string) => {
|
||||
const module = modules.find(module => module.dashedName === name);
|
||||
@@ -44,13 +53,38 @@ const isLinkModule = (name: string) => {
|
||||
};
|
||||
|
||||
const isLinkChapter = (name: string) => {
|
||||
const chapter = superBlockStructure.chapters.find(
|
||||
chapter => chapter.dashedName === name
|
||||
);
|
||||
const chapter = chapters.find(chapter => chapter.dashedName === name);
|
||||
|
||||
return chapter?.chapterType === 'exam';
|
||||
};
|
||||
|
||||
const getBlockToChapterMap = () => {
|
||||
const blockToChapterMap = new Map<string, string>();
|
||||
chapters.forEach(chapter => {
|
||||
chapter.modules.forEach(module => {
|
||||
module.blocks.forEach(block => {
|
||||
blockToChapterMap.set(block.dashedName, chapter.dashedName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return blockToChapterMap;
|
||||
};
|
||||
|
||||
const getBlockToModuleMap = () => {
|
||||
const blockToModuleMap = new Map<string, string>();
|
||||
modules.forEach(module => {
|
||||
module.blocks.forEach(block => {
|
||||
blockToModuleMap.set(block.dashedName, module.dashedName);
|
||||
});
|
||||
});
|
||||
|
||||
return blockToModuleMap;
|
||||
};
|
||||
|
||||
const blockToChapterMap = getBlockToChapterMap();
|
||||
const blockToModuleMap = getBlockToModuleMap();
|
||||
|
||||
const Chapter = ({ dashedName, children, isExpanded }: ChapterProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -89,64 +123,89 @@ const Module = ({ dashedName, children, isExpanded }: ModuleProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ComingSoon = ({ children }: { children: ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<li className='coming-soon'>
|
||||
<span className='badge'>{t('misc.coming-soon')}</span> {children}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const LinkBlock = ({
|
||||
superBlock,
|
||||
challenges
|
||||
}: {
|
||||
superBlock: SuperBlocks;
|
||||
challenges?: ChallengeNode['challenge'][];
|
||||
}) =>
|
||||
challenges?.length ? (
|
||||
<li className='link-block'>
|
||||
<Block
|
||||
block={challenges[0].block}
|
||||
blockType={challenges[0].blockType}
|
||||
challenges={challenges}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
</li>
|
||||
) : null;
|
||||
|
||||
export const SuperBlockAccordion = ({
|
||||
challenges,
|
||||
superBlock,
|
||||
chosenBlock
|
||||
}: SuperBlockTreeViewProps) => {
|
||||
const { allChapters, allBlocks } = 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 { t } = useTranslation();
|
||||
const { allChapters } = useMemo(() => {
|
||||
const populateBlocks = (blocks: { dashedName: string }[]) =>
|
||||
blocks.map(block => {
|
||||
const blockChallenges = challenges.filter(
|
||||
({ block: blockName }) => blockName === block.dashedName
|
||||
);
|
||||
|
||||
const allModules = uniqBy(allBlocks, 'module').map(
|
||||
({ module, chapter }) => ({
|
||||
name: module,
|
||||
chapter,
|
||||
blocks: allBlocks.filter(({ module: m }) => m === module)
|
||||
})
|
||||
);
|
||||
return {
|
||||
name: block.dashedName,
|
||||
blockType: blockChallenges[0]?.blockType ?? null,
|
||||
challenges: blockChallenges
|
||||
};
|
||||
});
|
||||
|
||||
const allChapters = uniqBy(allModules, 'chapter').map(({ chapter }) => ({
|
||||
name: chapter,
|
||||
modules: allModules.filter(({ chapter: c }) => c === chapter)
|
||||
const allChapters = chapters.map(chapter => ({
|
||||
name: chapter.dashedName,
|
||||
comingSoon: chapter.comingSoon,
|
||||
modules: chapter.modules.map((module: Module) => ({
|
||||
name: module.dashedName,
|
||||
comingSoon: module.comingSoon,
|
||||
blocks: populateBlocks(module.blocks)
|
||||
}))
|
||||
}));
|
||||
|
||||
return {
|
||||
allChapters,
|
||||
allModules,
|
||||
allBlocks
|
||||
};
|
||||
return { allChapters };
|
||||
}, [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;
|
||||
const expandedChapter = blockToChapterMap.get(chosenBlock);
|
||||
const expandedModule = blockToModuleMap.get(chosenBlock);
|
||||
|
||||
return (
|
||||
<ul className='super-block-accordion'>
|
||||
{allChapters.map(chapter => {
|
||||
if (isLinkChapter(chapter.name)) {
|
||||
const linkedChallenge = chapter.modules[0].blocks[0].challenges[0];
|
||||
// show coming soon on production, and all the challenges in dev
|
||||
if (chapter.comingSoon && !showUpcomingChanges) {
|
||||
return (
|
||||
<li key={chapter.name} className='link-chapter'>
|
||||
<Block
|
||||
block={linkedChallenge.block}
|
||||
blockType={linkedChallenge.blockType}
|
||||
challenges={[linkedChallenge]}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
</li>
|
||||
<ComingSoon key={chapter.name}>
|
||||
{t(`intro:full-stack-developer.chapters.${chapter.name}`)}
|
||||
</ComingSoon>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLinkChapter(chapter.name)) {
|
||||
return (
|
||||
<LinkBlock
|
||||
key={chapter.name}
|
||||
superBlock={superBlock}
|
||||
challenges={chapter.modules[0]?.blocks[0]?.challenges}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,17 +216,24 @@ export const SuperBlockAccordion = ({
|
||||
isExpanded={expandedChapter === chapter.name}
|
||||
>
|
||||
{chapter.modules.map(module => {
|
||||
if (isLinkModule(module.name)) {
|
||||
const linkedChallenge = module.blocks[0].challenges[0];
|
||||
// show coming soon on production, and all the challenges in dev
|
||||
if (module.comingSoon && !showUpcomingChanges) {
|
||||
return (
|
||||
<li key={module.name} className='link-module'>
|
||||
<Block
|
||||
block={linkedChallenge.block}
|
||||
blockType={linkedChallenge.blockType}
|
||||
challenges={[linkedChallenge]}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
</li>
|
||||
<ComingSoon key={chapter.name}>
|
||||
<span className='coming-soon-module'>
|
||||
{t(`intro:full-stack-developer.modules.${module.name}`)}
|
||||
</span>
|
||||
</ComingSoon>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLinkModule(module.name)) {
|
||||
return (
|
||||
<LinkBlock
|
||||
key={module.name}
|
||||
superBlock={superBlock}
|
||||
challenges={module.blocks[0]?.challenges}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,6 +244,7 @@ export const SuperBlockAccordion = ({
|
||||
isExpanded={expandedModule === module.name}
|
||||
>
|
||||
{module.blocks.map(block => (
|
||||
// maybe TODO: allow blocks to be "coming soon"
|
||||
<li key={block.name}>
|
||||
<Block
|
||||
block={block.name}
|
||||
|
||||
@@ -7,18 +7,22 @@ const schema = Joi.object()
|
||||
chapters: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
dashedName: Joi.string().regex(slugRE).required(),
|
||||
comingSoon: Joi.boolean().optional(),
|
||||
chapterType: Joi.valid('exam').optional(),
|
||||
modules: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
moduleType: Joi.valid('review', 'exam').optional(),
|
||||
dashedName: Joi.string().regex(slugRE).required(),
|
||||
blocks: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
dashedName: Joi.string().regex(slugRE).required()
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
modules: Joi.array()
|
||||
.items(
|
||||
Joi.object().keys({
|
||||
moduleType: Joi.valid('review', 'exam').optional(),
|
||||
comingSoon: Joi.boolean().optional(),
|
||||
dashedName: Joi.string().regex(slugRE).required(),
|
||||
blocks: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
dashedName: Joi.string().regex(slugRE).required()
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
.required()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"chapters": [
|
||||
{
|
||||
"dashedName": "freecodecamp",
|
||||
"comingSoon": true,
|
||||
"modules": [
|
||||
{
|
||||
"dashedName": "getting-started-with-freecodecamp",
|
||||
@@ -11,6 +12,7 @@
|
||||
},
|
||||
{
|
||||
"dashedName": "html",
|
||||
"comingSoon": true,
|
||||
"modules": [
|
||||
{
|
||||
"dashedName": "basic-html",
|
||||
@@ -71,6 +73,7 @@
|
||||
},
|
||||
{
|
||||
"dashedName": "css",
|
||||
"comingSoon": true,
|
||||
"modules": [
|
||||
{
|
||||
"dashedName": "computer-basics",
|
||||
@@ -276,6 +279,7 @@
|
||||
},
|
||||
{
|
||||
"dashedName": "javascript",
|
||||
"comingSoon": true,
|
||||
"modules": [
|
||||
{
|
||||
"dashedName": "code-editors",
|
||||
@@ -546,6 +550,7 @@
|
||||
|
||||
{
|
||||
"dashedName": "frontend-libraries",
|
||||
"comingSoon": true,
|
||||
"modules": [
|
||||
{
|
||||
"dashedName": "react-fundamentals",
|
||||
@@ -564,6 +569,7 @@
|
||||
},
|
||||
{
|
||||
"dashedName": "react-state-hooks-and-routing",
|
||||
"comingSoon": true,
|
||||
"blocks": [
|
||||
{ "dashedName": "quiz-react-state-and-hooks" },
|
||||
{ "dashedName": "quiz-advanced-react" }
|
||||
@@ -571,6 +577,7 @@
|
||||
},
|
||||
{
|
||||
"dashedName": "performance",
|
||||
"comingSoon": true,
|
||||
"blocks": [
|
||||
{ "dashedName": "review-web-performance" },
|
||||
{ "dashedName": "quiz-web-performance" }
|
||||
@@ -578,6 +585,7 @@
|
||||
},
|
||||
{
|
||||
"dashedName": "css-libraries-and-frameworks",
|
||||
"comingSoon": true,
|
||||
"blocks": [
|
||||
{ "dashedName": "review-css-libraries-and-frameworks" },
|
||||
{ "dashedName": "quiz-css-libraries-and-frameworks" }
|
||||
@@ -585,6 +593,7 @@
|
||||
},
|
||||
{
|
||||
"dashedName": "testing",
|
||||
"comingSoon": true,
|
||||
"blocks": [
|
||||
{ "dashedName": "review-testing" },
|
||||
{ "dashedName": "quiz-testing" }
|
||||
@@ -592,6 +601,7 @@
|
||||
},
|
||||
{
|
||||
"dashedName": "typescript-fundamentals",
|
||||
"comingSoon": true,
|
||||
"blocks": [
|
||||
{ "dashedName": "review-typescript" },
|
||||
{ "dashedName": "quiz-typescript" }
|
||||
@@ -606,6 +616,7 @@
|
||||
},
|
||||
{
|
||||
"dashedName": "relational-databases",
|
||||
"comingSoon": true,
|
||||
"modules": [
|
||||
{
|
||||
"dashedName": "bash-fundamentals",
|
||||
@@ -670,6 +681,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dashedName": "backend-javascript",
|
||||
"comingSoon": true,
|
||||
"modules": []
|
||||
},
|
||||
{
|
||||
"dashedName": "python",
|
||||
"comingSoon": true,
|
||||
"modules": []
|
||||
},
|
||||
{
|
||||
"chapterType": "exam",
|
||||
"dashedName": "exam-certified-full-stack-developer",
|
||||
|
||||
Reference in New Issue
Block a user