mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
feat(client):update accordion map (#63053)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
|
||||
import type { BlockLabel as BlockLabelType } from '../../../../../shared-dist/config/blocks';
|
||||
import { ProgressBar } from '../../../components/Progress/progress-bar';
|
||||
@@ -19,6 +20,8 @@ interface BlockHeaderProps {
|
||||
isExpanded: boolean;
|
||||
percentageCompleted: number;
|
||||
blockIntroArr?: string[];
|
||||
accordion?: boolean;
|
||||
blockUrl?: string;
|
||||
}
|
||||
|
||||
function BlockHeader({
|
||||
@@ -31,27 +34,37 @@ function BlockHeader({
|
||||
isCompleted,
|
||||
isExpanded,
|
||||
percentageCompleted,
|
||||
blockIntroArr
|
||||
blockIntroArr,
|
||||
accordion,
|
||||
blockUrl
|
||||
}: BlockHeaderProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h3 className='block-grid-title'>
|
||||
<button
|
||||
<Button
|
||||
aria-expanded={isExpanded ? 'true' : 'false'}
|
||||
aria-controls={`${blockDashed}-panel`}
|
||||
className='block-header'
|
||||
onClick={handleClick}
|
||||
{...(accordion && blockUrl ? { href: blockUrl } : {})}
|
||||
>
|
||||
<span className='block-header-button-text map-title'>
|
||||
{accordion &&
|
||||
(blockUrl ? (
|
||||
<span className='aligner-dash'></span>
|
||||
) : (
|
||||
<DropDown />
|
||||
))}
|
||||
<CheckMark isCompleted={isCompleted} />
|
||||
{blockLabel && <BlockLabel blockLabel={blockLabel} />}
|
||||
{!accordion && blockLabel && <BlockLabel blockLabel={blockLabel} />}
|
||||
<span>
|
||||
{blockTitle}
|
||||
<span className='sr-only'>, {courseCompletionStatus}</span>
|
||||
</span>
|
||||
<DropDown />
|
||||
{accordion && blockLabel && <BlockLabel blockLabel={blockLabel} />}
|
||||
{!accordion && <DropDown />}
|
||||
</span>
|
||||
{!isExpanded && !isCompleted && completedCount > 0 && (
|
||||
{!accordion && !isExpanded && !isCompleted && completedCount > 0 && (
|
||||
<div aria-hidden='true' className='progress-wrapper'>
|
||||
<div>
|
||||
<ProgressBar now={percentageCompleted} />
|
||||
@@ -59,7 +72,7 @@ function BlockHeader({
|
||||
<span>{`${percentageCompleted}%`}</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</h3>
|
||||
{isExpanded && !isEmpty(blockIntroArr) && (
|
||||
<BlockIntros intros={blockIntroArr as string[]} />
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
ChallengesWithDialogues
|
||||
} from './challenges';
|
||||
import BlockLabel from './block-label';
|
||||
import BlockIntros from './block-intros';
|
||||
import BlockHeader from './block-header';
|
||||
|
||||
import '../intro.css';
|
||||
@@ -74,6 +73,7 @@ interface BlockProps {
|
||||
superBlock: SuperBlocks;
|
||||
t: TFunction;
|
||||
toggleBlock: typeof toggleBlock;
|
||||
accordion?: boolean;
|
||||
}
|
||||
|
||||
export class Block extends Component<BlockProps> {
|
||||
@@ -113,7 +113,8 @@ export class Block extends Component<BlockProps> {
|
||||
challenges,
|
||||
isExpanded,
|
||||
superBlock,
|
||||
t
|
||||
t,
|
||||
accordion = false
|
||||
} = this.props;
|
||||
|
||||
let completedCount = 0;
|
||||
@@ -199,7 +200,6 @@ export class Block extends Component<BlockProps> {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<BlockIntros intros={blockIntroArr} />
|
||||
<button
|
||||
aria-expanded={isExpanded}
|
||||
className='map-title'
|
||||
@@ -255,7 +255,6 @@ export class Block extends Component<BlockProps> {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<BlockIntros intros={blockIntroArr} />
|
||||
<ChallengesList challenges={extendedChallenges} />
|
||||
</div>
|
||||
</Element>
|
||||
@@ -283,6 +282,8 @@ export class Block extends Component<BlockProps> {
|
||||
isCompleted={isBlockCompleted}
|
||||
isExpanded={isExpanded}
|
||||
percentageCompleted={percentageCompleted}
|
||||
accordion={accordion}
|
||||
blockIntroArr={!accordion ? blockIntroArr : undefined}
|
||||
/>
|
||||
|
||||
{isExpanded && (
|
||||
@@ -299,8 +300,8 @@ export class Block extends Component<BlockProps> {
|
||||
)}
|
||||
|
||||
<div id={`${block}-panel`}>
|
||||
<BlockIntros intros={blockIntroArr} />
|
||||
<GridMapChallenges
|
||||
jumpLink={!accordion}
|
||||
challenges={extendedChallenges}
|
||||
isProjectBlock={isProjectBlock}
|
||||
blockTitle={blockTitle}
|
||||
@@ -330,6 +331,8 @@ export class Block extends Component<BlockProps> {
|
||||
isCompleted={isBlockCompleted}
|
||||
isExpanded={isExpanded}
|
||||
percentageCompleted={percentageCompleted}
|
||||
accordion={accordion}
|
||||
blockIntroArr={!accordion ? blockIntroArr : undefined}
|
||||
/>
|
||||
|
||||
{isExpanded && (
|
||||
@@ -346,10 +349,10 @@ export class Block extends Component<BlockProps> {
|
||||
)}
|
||||
|
||||
<div id={`${block}-panel`}>
|
||||
<BlockIntros intros={blockIntroArr} />
|
||||
<ChallengesWithDialogues
|
||||
challenges={extendedChallenges}
|
||||
blockTitle={blockTitle}
|
||||
jumpLink={!accordion}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -406,7 +409,6 @@ export class Block extends Component<BlockProps> {
|
||||
</Link>
|
||||
</h3>
|
||||
</div>
|
||||
<BlockIntros intros={blockIntroArr} />
|
||||
</div>
|
||||
</Element>
|
||||
);
|
||||
@@ -414,13 +416,13 @@ export class Block extends Component<BlockProps> {
|
||||
/**
|
||||
* AccordionBlock is used as the block layout in new accordion style superblocks.
|
||||
*/
|
||||
const AccordionBlock = (
|
||||
const AccordionBlock = accordion ? (
|
||||
<>
|
||||
<Element name={block}>
|
||||
<span className='hide-scrollable-anchor'></span>
|
||||
</Element>
|
||||
<div
|
||||
className={`block block-grid challenge-grid-block ${isExpanded ? 'open' : ''}`}
|
||||
className={`block block-grid block-grid-no-border challenge-grid-block ${isExpanded ? 'open' : ''}`}
|
||||
onMouseOver={this.handleBlockHover}
|
||||
onFocus={this.handleBlockHover}
|
||||
>
|
||||
@@ -434,7 +436,7 @@ export class Block extends Component<BlockProps> {
|
||||
isCompleted={isBlockCompleted}
|
||||
isExpanded={isExpanded}
|
||||
percentageCompleted={percentageCompleted}
|
||||
blockIntroArr={blockIntroArr}
|
||||
accordion={accordion}
|
||||
/>
|
||||
|
||||
{isExpanded && (
|
||||
@@ -458,6 +460,62 @@ export class Block extends Component<BlockProps> {
|
||||
challenges={extendedChallenges}
|
||||
blockTitle={blockTitle}
|
||||
isProjectBlock={isProjectBlock}
|
||||
jumpLink={false}
|
||||
/>
|
||||
) : (
|
||||
<ChallengesList challenges={extendedChallenges} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Element name={block}>
|
||||
<span className='hide-scrollable-anchor'></span>
|
||||
</Element>
|
||||
<div
|
||||
className={`block block-grid challenge-grid-block ${isExpanded ? 'open' : ''}`}
|
||||
onMouseOver={this.handleBlockHover}
|
||||
onFocus={this.handleBlockHover}
|
||||
>
|
||||
<BlockHeader
|
||||
blockDashed={block}
|
||||
blockTitle={blockTitle}
|
||||
blockLabel={blockLabel}
|
||||
completedCount={completedCount}
|
||||
courseCompletionStatus={courseCompletionStatus()}
|
||||
handleClick={this.handleBlockClick}
|
||||
isCompleted={isBlockCompleted}
|
||||
isExpanded={isExpanded}
|
||||
percentageCompleted={percentageCompleted}
|
||||
accordion={accordion}
|
||||
blockIntroArr={blockIntroArr}
|
||||
/>
|
||||
|
||||
{isExpanded && (
|
||||
<div className='accordion-block-expanded'>
|
||||
{!isAudited && (
|
||||
<div className='tags-wrapper'>
|
||||
<Link
|
||||
className='cert-tag'
|
||||
to={t('links:help-translate-link-url')}
|
||||
>
|
||||
{t('misc.translation-pending')}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
id={`${block}-panel`}
|
||||
className={isGridBlock ? 'challenge-grid-block-panel' : ''}
|
||||
>
|
||||
{isGridBlock ? (
|
||||
<GridMapChallenges
|
||||
jumpLink={true}
|
||||
challenges={extendedChallenges}
|
||||
blockTitle={blockTitle}
|
||||
isProjectBlock={isProjectBlock}
|
||||
/>
|
||||
) : (
|
||||
<ChallengesList challenges={extendedChallenges} />
|
||||
@@ -469,10 +527,31 @@ export class Block extends Component<BlockProps> {
|
||||
</>
|
||||
);
|
||||
|
||||
const LinkBlock = (
|
||||
<>
|
||||
<Element name={block}>
|
||||
<span className='hide-scrollable-anchor'></span>
|
||||
</Element>
|
||||
<BlockHeader
|
||||
blockDashed={block}
|
||||
blockTitle={blockTitle}
|
||||
blockLabel={blockLabel}
|
||||
completedCount={completedCount}
|
||||
courseCompletionStatus={courseCompletionStatus()}
|
||||
handleClick={this.handleBlockClick}
|
||||
isCompleted={isBlockCompleted}
|
||||
isExpanded={isExpanded}
|
||||
percentageCompleted={percentageCompleted}
|
||||
accordion={accordion}
|
||||
blockUrl={challenges[0].fields.slug}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const layoutToComponent = {
|
||||
[BlockLayouts.ChallengeGrid]: AccordionBlock,
|
||||
[BlockLayouts.ChallengeList]: AccordionBlock,
|
||||
[BlockLayouts.Link]: AccordionBlock,
|
||||
[BlockLayouts.Link]: accordion ? LinkBlock : AccordionBlock,
|
||||
[BlockLayouts.ProjectList]: ProjectListBlock,
|
||||
[BlockLayouts.LegacyLink]: LegacyLinkBlock,
|
||||
[BlockLayouts.LegacyChallengeList]: LegacyChallengeListBlock,
|
||||
|
||||
@@ -22,6 +22,10 @@ interface ChallengesProps {
|
||||
challenges: ChallengeInfo[];
|
||||
}
|
||||
|
||||
interface JumpLinkProps {
|
||||
jumpLink?: boolean;
|
||||
}
|
||||
|
||||
interface BlockTitleProps {
|
||||
blockTitle: string;
|
||||
}
|
||||
@@ -122,16 +126,18 @@ const LinkToFirstIncompleteChallenge = ({
|
||||
export const GridMapChallenges = ({
|
||||
challenges,
|
||||
blockTitle,
|
||||
isProjectBlock
|
||||
}: ChallengesProps & BlockTitleProps & IsProjectBlockProps) => {
|
||||
isProjectBlock,
|
||||
jumpLink
|
||||
}: ChallengesProps & BlockTitleProps & IsProjectBlockProps & JumpLinkProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinkToFirstIncompleteChallenge
|
||||
challenges={challenges}
|
||||
blockTitle={blockTitle}
|
||||
/>
|
||||
{jumpLink && (
|
||||
<LinkToFirstIncompleteChallenge
|
||||
challenges={challenges}
|
||||
blockTitle={blockTitle}
|
||||
/>
|
||||
)}
|
||||
<nav aria-label={t('aria.steps-for', { blockTitle })}>
|
||||
<ul className={`map-challenges-ul map-challenges-grid`}>
|
||||
{challenges.map(challenge => (
|
||||
@@ -161,16 +167,19 @@ export const GridMapChallenges = ({
|
||||
|
||||
export const ChallengesWithDialogues = ({
|
||||
challenges,
|
||||
blockTitle
|
||||
}: ChallengesProps & BlockTitleProps) => {
|
||||
blockTitle,
|
||||
jumpLink
|
||||
}: ChallengesProps & BlockTitleProps & JumpLinkProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinkToFirstIncompleteChallenge
|
||||
challenges={challenges}
|
||||
blockTitle={blockTitle}
|
||||
/>
|
||||
{jumpLink && (
|
||||
<LinkToFirstIncompleteChallenge
|
||||
challenges={challenges}
|
||||
blockTitle={blockTitle}
|
||||
/>
|
||||
)}
|
||||
|
||||
<nav aria-label={t('aria.dialogues-and-tasks-for', { blockTitle })}>
|
||||
<ul className={`map-challenges-ul map-challenges-grid`}>
|
||||
{challenges.map(challenge => (
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
.super-block-accordion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.super-block-accordion ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.super-block-accordion li {
|
||||
@@ -17,14 +17,14 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter,
|
||||
.super-block-accordion .link-block {
|
||||
background-color: var(--primary-background);
|
||||
.super-block-accordion .chapter {
|
||||
border: 3px solid var(--tertiary-background);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-button {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
background-color: var(--primary-background);
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
@@ -32,12 +32,43 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.super-block-accordion .aligner-dash {
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.super-block-accordion .map-icon {
|
||||
width: 26px;
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-button .chapter-button-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: start;
|
||||
column-gap: 10px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.super-block-accordion .block-grid {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.super-block-accordion .block-grid .block-header[aria-expanded='true'] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
button .block-header-button-text {
|
||||
font-weight: normal;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.super-block-accordion .map-title,
|
||||
.super-block-accordion .block-header-button-text {
|
||||
gap: 10px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.super-block-accordion .block-header-button-text {
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-button .chapter-button-right {
|
||||
@@ -56,15 +87,16 @@
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.super-block-accordion .module-button {
|
||||
.super-block-accordion .module-button,
|
||||
.super-block-accordion .block-header {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
font-size: 1.25em;
|
||||
padding: 16px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: normal;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@@ -88,7 +120,8 @@
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-button:hover,
|
||||
.super-block-accordion .module-button:hover {
|
||||
.super-block-accordion .module-button:hover,
|
||||
.super-block-accordion .block-header:hover {
|
||||
background-color: var(--tertiary-background);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
@@ -104,17 +137,60 @@
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-panel {
|
||||
padding: 0 16px;
|
||||
background-color: var(--primary-background);
|
||||
border-top: 1px solid var(--tertiary-background);
|
||||
}
|
||||
|
||||
.super-block-accordion .module-panel {
|
||||
padding: 0 24px;
|
||||
.super-block-accordion .module-panel .block-header {
|
||||
padding: 10px 0px;
|
||||
padding-inline-start: 26px;
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-panel > li {
|
||||
border-bottom: 4px solid var(--secondary-background);
|
||||
.super-block-accordion .block-label {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.super-block-accordion
|
||||
.module-panel
|
||||
.accordion-block-expanded
|
||||
.map-challenge-title
|
||||
> * {
|
||||
padding-inline: 64px 10px;
|
||||
padding-block: 10px;
|
||||
}
|
||||
|
||||
.super-block-accordion
|
||||
.module-panel
|
||||
.accordion-block-expanded
|
||||
.map-challenge-title
|
||||
> .map-grid-item {
|
||||
padding: 0;
|
||||
height: 2.1rem;
|
||||
width: 2.1rem;
|
||||
}
|
||||
|
||||
.super-block-accordion
|
||||
.module-panel
|
||||
.accordion-block-expanded
|
||||
.map-challenges-grid {
|
||||
padding-block: 10px;
|
||||
padding-inline-start: 64px;
|
||||
grid-template-columns: repeat(auto-fill, 2.1rem);
|
||||
}
|
||||
|
||||
.super-block-accordion .map-challenge-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.super-block-accordion .accordion-block-expanded .map-challenges-grid,
|
||||
.super-block-accordion .challenge-grid-block .challenge-grid-block-panel {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.super-block-accordion .map-challenge-title a,
|
||||
.super-block-accordion .accordion-block-expanded {
|
||||
padding: 0;
|
||||
}
|
||||
.super-block-accordion .chapter-panel > li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -124,6 +200,34 @@
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.super-block-accordion .module-panel,
|
||||
.super-block-accordion .accordion-block-expanded {
|
||||
position: relative;
|
||||
}
|
||||
.super-block-accordion .module-panel::before {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background: var(--quaternary-background);
|
||||
}
|
||||
|
||||
.super-block-accordion .accordion-block-expanded::before {
|
||||
position: absolute;
|
||||
left: 32px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
content: '';
|
||||
background: var(--quaternary-background);
|
||||
}
|
||||
|
||||
.super-block-accordion .chapter-steps {
|
||||
font-size: 0.85rem;
|
||||
color: var(--quaternary-color);
|
||||
}
|
||||
|
||||
.super-block-accordion .badge {
|
||||
color: var(--quaternary-color);
|
||||
border: 1px solid var(--quaternary-color);
|
||||
@@ -140,8 +244,44 @@
|
||||
}
|
||||
|
||||
.super-block-accordion .module-intro {
|
||||
margin-left: 17px;
|
||||
margin-top: 16px;
|
||||
padding: 16px 16px 0px 32px;
|
||||
}
|
||||
|
||||
.super-block-accordion .module-intro p {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.super-block-accordion
|
||||
.chapter-panel
|
||||
.chapter-button[aria-expanded='false']
|
||||
.dropdown-icon,
|
||||
.super-block-accordion
|
||||
.chapter-panel
|
||||
.module-button[aria-expanded='false']
|
||||
.dropdown-icon,
|
||||
.super-block-accordion
|
||||
.chapter-panel
|
||||
.block-grid
|
||||
.block-header[aria-expanded='false']
|
||||
.dropdown-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.super-block-accordion
|
||||
.chapter-panel
|
||||
.chapter-button[aria-expanded='true']
|
||||
.dropdown-icon,
|
||||
.super-block-accordion
|
||||
.chapter-panel
|
||||
.module-button[aria-expanded='true']
|
||||
.dropdown-icon,
|
||||
.super-block-accordion
|
||||
.chapter-panel
|
||||
.block-grid
|
||||
.block-header[aria-expanded='true']
|
||||
.dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.full-width-container {
|
||||
|
||||
@@ -8,6 +8,7 @@ import DropDown from '../../../assets/icons/dropdown';
|
||||
import type { ChapterBasedSuperBlockStructure } from '../../../redux/prop-types';
|
||||
import { ChapterIcon } from '../../../assets/chapter-icon';
|
||||
import { type Chapter } from '../../../../../shared-dist/config/chapters';
|
||||
import { Link } from '../../../components/helpers';
|
||||
import {
|
||||
BlockLayouts,
|
||||
BlockLabel
|
||||
@@ -30,6 +31,8 @@ interface ChapterProps {
|
||||
totalSteps: number;
|
||||
completedSteps: number;
|
||||
superBlock: SuperBlocks;
|
||||
isLinkChapter?: boolean;
|
||||
examSlug?: string;
|
||||
}
|
||||
|
||||
interface ModuleProps {
|
||||
@@ -39,6 +42,7 @@ interface ModuleProps {
|
||||
totalSteps: number;
|
||||
completedSteps: number;
|
||||
superBlock: SuperBlocks;
|
||||
comingSoon: boolean;
|
||||
}
|
||||
|
||||
interface Challenge {
|
||||
@@ -53,6 +57,25 @@ interface Challenge {
|
||||
superBlock: SuperBlocks;
|
||||
}
|
||||
|
||||
interface PopulatedBlock {
|
||||
name: string;
|
||||
blockLabel: BlockLabel | null;
|
||||
challenges: Challenge[];
|
||||
}
|
||||
|
||||
interface PopulatedModule {
|
||||
name: string;
|
||||
comingSoon?: boolean;
|
||||
moduleType?: string;
|
||||
blocks: PopulatedBlock[];
|
||||
}
|
||||
|
||||
interface PopulatedChapter {
|
||||
name: string;
|
||||
comingSoon?: boolean;
|
||||
modules: PopulatedModule[];
|
||||
}
|
||||
|
||||
interface SuperBlockAccordionProps {
|
||||
challenges: Challenge[];
|
||||
superBlock: SuperBlocks;
|
||||
@@ -68,10 +91,50 @@ const Chapter = ({
|
||||
comingSoon,
|
||||
totalSteps,
|
||||
completedSteps,
|
||||
superBlock
|
||||
superBlock,
|
||||
isLinkChapter,
|
||||
examSlug
|
||||
}: ChapterProps) => {
|
||||
const { t } = useTranslation();
|
||||
const isComplete = completedSteps === totalSteps;
|
||||
const chapterLabel = t(`intro:${superBlock}.chapters.${dashedName}`);
|
||||
|
||||
const chapterButtonContent = (
|
||||
<>
|
||||
<div className='chapter-button-left'>
|
||||
<span className='checkmark-wrap chapter-checkmark-wrap'>
|
||||
<CheckMark isCompleted={isComplete} />
|
||||
</span>
|
||||
<ChapterIcon className='map-icon' chapter={dashedName as FsdChapters} />
|
||||
{chapterLabel}
|
||||
</div>
|
||||
<div className='chapter-button-right'>
|
||||
{!comingSoon && (
|
||||
<span className='chapter-steps'>
|
||||
{t('learn.steps-completed', {
|
||||
totalSteps,
|
||||
completedSteps
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
<span className='dropdown-wrap'>{!isLinkChapter && <DropDown />}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (isLinkChapter && examSlug) {
|
||||
return (
|
||||
<li className='chapter'>
|
||||
<Link
|
||||
className='chapter-button'
|
||||
data-playwright-test-label='chapter-button'
|
||||
to={examSlug}
|
||||
>
|
||||
{chapterButtonContent}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Disclosure as='li' className='chapter' defaultOpen={isExpanded}>
|
||||
@@ -79,35 +142,13 @@ const Chapter = ({
|
||||
className='chapter-button'
|
||||
data-playwright-test-label='chapter-button'
|
||||
>
|
||||
<div className='chapter-button-left'>
|
||||
<ChapterIcon
|
||||
className='map-icon'
|
||||
chapter={dashedName as FsdChapters}
|
||||
/>
|
||||
{t(`intro:${superBlock}.chapters.${dashedName}`)}
|
||||
</div>
|
||||
<div className='chapter-button-right'>
|
||||
{!comingSoon && (
|
||||
<>
|
||||
<span className='chapter-steps'>
|
||||
{t('learn.steps-completed', {
|
||||
totalSteps,
|
||||
completedSteps
|
||||
})}
|
||||
</span>
|
||||
<span className='checkmark-wrap chapter-checkmark-wrap'>
|
||||
<CheckMark isCompleted={isComplete} />
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className='dropdown-wrap'>
|
||||
<DropDown />
|
||||
</span>
|
||||
</div>
|
||||
{chapterButtonContent}
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as='ul' className='chapter-panel'>
|
||||
{children}
|
||||
</Disclosure.Panel>
|
||||
{!isLinkChapter && !examSlug && (
|
||||
<Disclosure.Panel as='ul' className='chapter-panel'>
|
||||
{children}
|
||||
</Disclosure.Panel>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
@@ -118,10 +159,19 @@ const Module = ({
|
||||
isExpanded,
|
||||
totalSteps,
|
||||
completedSteps,
|
||||
superBlock
|
||||
superBlock,
|
||||
comingSoon
|
||||
}: ModuleProps) => {
|
||||
const { t } = useTranslation();
|
||||
const isComplete = completedSteps === totalSteps;
|
||||
const isComplete = totalSteps === 0 ? false : completedSteps === totalSteps;
|
||||
const { note, intro } = t(`intro:${superBlock}.module-intros.${dashedName}`, {
|
||||
returnObjects: true
|
||||
}) as {
|
||||
note: string;
|
||||
intro: string[];
|
||||
};
|
||||
|
||||
const showModuleContent = !(comingSoon && !showUpcomingChanges);
|
||||
|
||||
return (
|
||||
<Disclosure as='li' defaultOpen={isExpanded}>
|
||||
@@ -130,44 +180,52 @@ const Module = ({
|
||||
<span className='dropdown-wrap'>
|
||||
<DropDown />
|
||||
</span>
|
||||
{t(`intro:${superBlock}.modules.${dashedName}`)}
|
||||
</div>
|
||||
<div className='module-button-right'>
|
||||
<span className='module-steps'>
|
||||
{t('learn.steps-completed', {
|
||||
totalSteps,
|
||||
completedSteps
|
||||
})}
|
||||
</span>
|
||||
<span className='checkmark-wrap'>
|
||||
<CheckMark isCompleted={isComplete} />
|
||||
</span>
|
||||
{t(`intro:${superBlock}.modules.${dashedName}`)}
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as='ul' className='module-panel'>
|
||||
{children}
|
||||
{comingSoon && (
|
||||
<div className='module-intro'>
|
||||
{note && (
|
||||
<p>
|
||||
<b>{note}</b>
|
||||
</p>
|
||||
)}
|
||||
{intro?.length && intro.map(ntro => <p key={ntro}>{ntro}</p>)}
|
||||
</div>
|
||||
)}
|
||||
{showModuleContent && children}
|
||||
</Disclosure.Panel>
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
|
||||
const LinkBlock = ({
|
||||
const LinkModule = ({
|
||||
superBlock,
|
||||
challenges
|
||||
challenges,
|
||||
accordion
|
||||
}: {
|
||||
superBlock: SuperBlocks;
|
||||
challenges?: Challenge[];
|
||||
}) =>
|
||||
challenges?.length ? (
|
||||
accordion: boolean;
|
||||
}) => {
|
||||
if (!challenges?.length) return null;
|
||||
|
||||
return (
|
||||
<li className='link-block'>
|
||||
<Block
|
||||
block={challenges[0].block}
|
||||
blockLabel={challenges[0].blockLabel}
|
||||
challenges={challenges}
|
||||
superBlock={superBlock}
|
||||
accordion={accordion}
|
||||
/>
|
||||
</li>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
export const SuperBlockAccordion = ({
|
||||
challenges,
|
||||
@@ -185,7 +243,11 @@ export const SuperBlockAccordion = ({
|
||||
const isLinkModule = (name: string) => {
|
||||
const module = modules.find(module => module.dashedName === name);
|
||||
|
||||
return module?.moduleType === 'review' || module?.moduleType === 'exam';
|
||||
return (
|
||||
module?.moduleType === 'review' ||
|
||||
module?.moduleType === 'exam' ||
|
||||
module?.moduleType === 'quiz'
|
||||
);
|
||||
};
|
||||
|
||||
const getBlockToChapterMap = () => {
|
||||
@@ -214,11 +276,8 @@ export const SuperBlockAccordion = ({
|
||||
|
||||
const blockToChapterMap = getBlockToChapterMap();
|
||||
const blockToModuleMap = getBlockToModuleMap();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { allChapters } = useMemo(() => {
|
||||
const chapters = superBlockStructure.chapters;
|
||||
const populateBlocks = (blocks: string[]) =>
|
||||
const allChapters = useMemo<PopulatedChapter[]>(() => {
|
||||
const populateBlocks = (blocks: string[]): PopulatedBlock[] =>
|
||||
blocks.map(block => {
|
||||
const blockChallenges = challenges.filter(
|
||||
({ block: blockName }) => blockName === block
|
||||
@@ -231,7 +290,7 @@ export const SuperBlockAccordion = ({
|
||||
};
|
||||
});
|
||||
|
||||
const allChapters = chapters.map(chapter => ({
|
||||
return superBlockStructure.chapters.map((chapter: Chapter) => ({
|
||||
name: chapter.dashedName,
|
||||
comingSoon: chapter.comingSoon,
|
||||
modules: chapter.modules.map((module: Module) => ({
|
||||
@@ -241,13 +300,12 @@ export const SuperBlockAccordion = ({
|
||||
blocks: populateBlocks(module.blocks)
|
||||
}))
|
||||
}));
|
||||
|
||||
return { allChapters };
|
||||
}, [challenges, superBlockStructure.chapters]);
|
||||
|
||||
// Expand the outer layers in order to reveal the chosen block.
|
||||
const expandedChapter = blockToChapterMap.get(chosenBlock);
|
||||
const expandedModule = blockToModuleMap.get(chosenBlock);
|
||||
const accordion = true;
|
||||
|
||||
return (
|
||||
<ul className='super-block-accordion'>
|
||||
@@ -261,10 +319,25 @@ export const SuperBlockAccordion = ({
|
||||
});
|
||||
|
||||
const chapterStepIdsSet = new Set(chapterStepIds);
|
||||
|
||||
const completedStepsInChapter = new Set(
|
||||
completedChallengeIds.filter(id => chapterStepIdsSet.has(id))
|
||||
).size;
|
||||
|
||||
const [firstChapterModule] = chapter.modules;
|
||||
|
||||
const [firstModuleBlock] = firstChapterModule?.blocks ?? [];
|
||||
|
||||
const isLinkChapter =
|
||||
chapter.modules.length === 1 &&
|
||||
firstChapterModule?.blocks.length === 1 &&
|
||||
firstModuleBlock?.blockLabel === BlockLabel.exam &&
|
||||
firstModuleBlock.challenges.length === 1;
|
||||
|
||||
const examSlug = isLinkChapter
|
||||
? firstModuleBlock?.challenges[0]?.fields.slug
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Chapter
|
||||
key={chapter.name}
|
||||
@@ -276,57 +349,23 @@ export const SuperBlockAccordion = ({
|
||||
totalSteps={chapterStepIds.length}
|
||||
completedSteps={completedStepsInChapter}
|
||||
superBlock={superBlock}
|
||||
isLinkChapter={isLinkChapter}
|
||||
examSlug={examSlug}
|
||||
>
|
||||
{chapter.modules.map(module => {
|
||||
if (module.comingSoon && !showUpcomingChanges) {
|
||||
if (module.moduleType === 'review') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { note, intro } = t(
|
||||
`intro:${superBlock}.module-intros.${module.name}`,
|
||||
{ returnObjects: true }
|
||||
) as {
|
||||
note: string;
|
||||
intro: string[];
|
||||
};
|
||||
|
||||
return (
|
||||
<Disclosure
|
||||
key={module.name}
|
||||
as='li'
|
||||
defaultOpen={expandedModule === module.name}
|
||||
>
|
||||
<Disclosure.Button className='module-button'>
|
||||
<div className='module-button-left'>
|
||||
<span className='dropdown-wrap'>
|
||||
<DropDown />
|
||||
</span>
|
||||
{t(`intro:${superBlock}.modules.${module.name}`)}
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as='ul' className='module-panel'>
|
||||
<div className='module-intro'>
|
||||
{note && (
|
||||
<p>
|
||||
<b>{note}</b>
|
||||
</p>
|
||||
)}
|
||||
{intro &&
|
||||
intro.length > 0 &&
|
||||
intro.map(ntro => <p key={ntro}>{ntro}</p>)}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLinkModule(module.name)) {
|
||||
return (
|
||||
<LinkBlock
|
||||
<LinkModule
|
||||
key={module.name}
|
||||
superBlock={superBlock}
|
||||
challenges={module.blocks[0]?.challenges}
|
||||
accordion={accordion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -349,6 +388,7 @@ export const SuperBlockAccordion = ({
|
||||
totalSteps={moduleStepIds.length}
|
||||
completedSteps={completedStepsInModule}
|
||||
superBlock={superBlock}
|
||||
comingSoon={!!module.comingSoon}
|
||||
>
|
||||
{module.blocks.map(block => (
|
||||
// maybe TODO: allow blocks to be "coming soon"
|
||||
@@ -358,6 +398,7 @@ export const SuperBlockAccordion = ({
|
||||
blockLabel={block.blockLabel}
|
||||
challenges={block.challenges}
|
||||
superBlock={superBlock}
|
||||
accordion={accordion}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -937,6 +937,7 @@
|
||||
{ "dashedName": "capstone-project", "comingSoon": true, "blocks": [] },
|
||||
{
|
||||
"dashedName": "certified-full-stack-developer-exam",
|
||||
"moduleType": "exam",
|
||||
"comingSoon": true,
|
||||
"blocks": ["exam-certified-full-stack-developer"]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ test.describe('Super Block Page - Authenticated User', () => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem(
|
||||
'currentChallengeId',
|
||||
'660ee6e3a242da6bd579de69' // JS Pyramid Generator step 2
|
||||
'62a3b3eab50e193608c19fc6' // Learn Basic JavaScript by Building a Role Playing Game step 9
|
||||
);
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ test.describe('Super Block Page - Authenticated User', () => {
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Learn Introductory JavaScript by Building a Pyramid Generator'
|
||||
name: 'Learn Basic JavaScript by Building a Role Playing Game'
|
||||
})
|
||||
).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
@@ -124,14 +124,14 @@ test.describe('Super Block Page - Authenticated User', () => {
|
||||
// Module
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: /Basic CSS \d+ of \d+ steps complete/
|
||||
name: 'Basic CSS'
|
||||
})
|
||||
).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
// Block
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: /Workshop Design a Cafe Menu/
|
||||
name: 'Design a Cafe Menu'
|
||||
})
|
||||
).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
@@ -156,14 +156,14 @@ test.describe('Super Block Page - Authenticated User', () => {
|
||||
// Basic HTML module
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: /Basic HTML \d+ of \d+ steps complete/
|
||||
name: 'Basic HTML'
|
||||
})
|
||||
).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
// Understanding HTML Attributes block
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: /Theory Understanding HTML Attributes/
|
||||
name: 'Understanding HTML Attributes'
|
||||
})
|
||||
).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
@@ -183,14 +183,14 @@ test.describe('Super Block Page - Authenticated User', () => {
|
||||
// First module
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: /Basic HTML \d+ of \d+ steps complete/
|
||||
name: 'Basic HTML'
|
||||
})
|
||||
).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
// First block
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: /Build a Curriculum Outline/
|
||||
name: 'Build a Curriculum Outline'
|
||||
})
|
||||
).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
@@ -212,7 +212,7 @@ test.describe('Super Block Page - Authenticated User', () => {
|
||||
// Cat Blog Page block
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Workshop Build a Cat Blog Page'
|
||||
name: 'Build a Cat Blog Page'
|
||||
})
|
||||
).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
@@ -258,7 +258,7 @@ test.describe('Super Block Page - Unauthenticated User', () => {
|
||||
// First module
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: /Basic HTML \d+ of \d+ steps complete/
|
||||
name: 'Basic HTML'
|
||||
})
|
||||
).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user