mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-10 15:03:12 -04: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
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user