feat(curriculum, client): add catalog (#60951)

This commit is contained in:
Tom
2025-06-26 15:08:36 -05:00
committed by GitHub
parent 10e3004403
commit 3af161450f
39 changed files with 1504 additions and 12 deletions

View File

@@ -1,7 +1,10 @@
import fs from 'fs';
import { setup } from 'jest-json-schema-extended';
import { availableLangs, LangNames, LangCodes } from '../../shared/config/i18n';
import { SuperBlocks } from '../../shared/config/curriculum';
import {
catalogSuperBlocks,
SuperBlocks
} from '../../shared/config/curriculum';
import intro from './locales/english/intro.json';
setup();
@@ -9,6 +12,7 @@ setup();
interface Intro {
[key: string]: {
title: string;
summary?: string[];
intro: string[];
blocks: {
[block: string]: {
@@ -74,6 +78,12 @@ describe('Intro file structure tests:', () => {
const superblocks = Object.values(SuperBlocks);
for (const superBlock of superblocks) {
expect(typeof typedIntro[superBlock].title).toBe('string');
// catalog superblocks should have a summary
if (catalogSuperBlocks.includes(superBlock)) {
expect(typedIntro[superBlock].intro).toBeInstanceOf(Array);
}
expect(typedIntro[superBlock].intro).toBeInstanceOf(Array);
expect(typedIntro[superBlock].blocks).toBeInstanceOf(Object);
const blocks = Object.keys(typedIntro[superBlock].blocks);

View File

@@ -4553,6 +4553,49 @@
}
}
},
"basic-html": {
"title": "Basic HTML",
"summary": [
"Learn how to build simple webpages using HTML tags to add text, images, and links."
],
"intro": ["Larger intro for the superblock page."],
"blocks": {
"cat-photo-app": {
"title": "Build a Cat Photo App",
"intro": [
"HTML tags give a webpage its structure. You can use HTML tags to add photos, buttons, and other elements to your webpage.",
"In this course, you'll learn the most common HTML tags by building your own cat photo app."
]
},
"recipe-page": {
"title": "Build a Recipe Page",
"intro": [
"In this lab, you'll review HTML basics by creating a web page of your favorite recipe. You'll create an HTML boilerplate and work with headings, lists, images, and more."
]
}
}
},
"semantic-html": {
"title": "Semantic HTML",
"summary": [
"Discover how to write cleaner, more meaningful HTML using semantic tags that improve structure, accessibility, and SEO."
],
"intro": ["Larger intro for the superblock page."],
"blocks": {
"cat-blog-page": {
"title": "Build a Cat Blog Page",
"intro": [
"In this workshop, you will build an HTML only blog page using semantic elements including the <code>main</code>, <code>nav</code>, <code>article</code> and <code>footer</code> elements."
]
},
"event-hub": {
"title": "Build an Event Hub",
"intro": [
"In this lab, you'll build an event hub and review semantic elements like <code>header</code>, <code>nav</code>, <code>article</code>, and more."
]
}
}
},
"dev-playground": {
"title": "Dev Playground",
"intro": ["Playground for creating and testing challenges"],

View File

@@ -86,6 +86,7 @@
"click-start-course": "Start the course",
"click-start-project": "Start the project",
"click-start-exam": "Start the exam",
"go-to-course": "Go to course",
"change-language": "Change Language",
"resume-project": "Resume project",
"start-project": "Start project",
@@ -175,6 +176,7 @@
"legacy-curriculum-heading": "Our archived coursework:",
"next-heading": "Try our beta curriculum:",
"upcoming-heading": "Upcoming curriculum:",
"catalog-heading": "Explore our Catalog:",
"faq": "Frequently asked questions:",
"faqs": [
{
@@ -1245,5 +1247,15 @@
"exit": "Exit the survey",
"two-questions": "Congratulations on getting this far. Before you can start the exam, please answer these two short survey questions."
}
},
"curriculum": {
"catalog": {
"title": "Explore our Catalog",
"levels": {
"beginner": "Beginner",
"intermediate": "Intermediate",
"advanced": "Advanced"
}
}
}
}

View File

@@ -47,6 +47,8 @@ const iconMap = {
[SuperBlocks.A2Chinese]: A2EnglishIcon,
[SuperBlocks.RosettaCode]: RosettaCodeIcon,
[SuperBlocks.PythonForEverybody]: PythonIcon,
[SuperBlocks.BasicHtml]: Code,
[SuperBlocks.SemanticHtml]: Code,
[SuperBlocks.DevPlayground]: Code
};

View File

@@ -42,7 +42,8 @@ const superBlockHeadings: { [key in SuperBlockStage]: string } = {
[SuperBlockStage.Extra]: 'landing.interview-prep-heading',
[SuperBlockStage.Legacy]: 'landing.legacy-curriculum-heading',
[SuperBlockStage.Next]: 'landing.next-heading',
[SuperBlockStage.Upcoming]: 'landing.upcoming-heading'
[SuperBlockStage.Upcoming]: 'landing.upcoming-heading',
[SuperBlockStage.Catalog]: 'landing.catalog-heading'
};
const mapStateToProps = createSelector(

View File

@@ -0,0 +1,22 @@
.catalog-wrap {
display: flex;
gap: 2rem;
justify-content: space-evenly;
flex-wrap: wrap;
}
.catalog-item {
padding: 1rem;
background-color: var(--primary-background);
width: 400px;
min-height: 300px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.catalog-item-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
}

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Col, Spacer } from '@freecodecamp/ui';
import { ButtonLink } from '../components/helpers';
import { catalog } from '../../../shared/config/catalog';
import { showUpcomingChanges } from '../../config/env.json';
import FourOhFour from '../components/FourOhFour';
import './catalog.css';
const CatalogPage = () => {
const { t } = useTranslation();
return showUpcomingChanges ? (
<main>
<Spacer size='l' />
<h1 className='text-center'>{t('curriculum.catalog.title')}</h1>
<Spacer size='l' />
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<section className='catalog-wrap'>
{catalog.map(course => {
const { superBlock, level, hours } = course;
const { title, summary } = t(`intro:${superBlock}`, {
returnObjects: true
}) as {
title: string;
summary: string[];
};
return (
<div className='catalog-item' key={superBlock}>
<div className='catalog-item-top'>
<h2>{title}</h2>
<hr />
{summary.map(text => (
<p key='text'>{text}</p>
))}
</div>
<div className='catalog-item-bottom'>
<div>
{t(`curriculum.catalog.levels.${level}`)} &bull; {hours}{' '}
hours
</div>
<ButtonLink href={`/learn/${superBlock}`}>
{t('buttons.go-to-course')}
</ButtonLink>
</div>
</div>
);
})}
</section>
</Col>
<Spacer size='l' />
</main>
) : (
<FourOhFour />
);
};
export default CatalogPage;

View File

@@ -0,0 +1,11 @@
---
title: Introduction to Build a Cat Photo App
block: cat-photo-app
superBlock: basic-html
---
## Introduction to Build a Cat Photo App
HTML stands for HyperText Markup Language and it represents the content and structure of a web page.
In this workshop, you will learn how to work with basic HTML elements such as headings, paragraphs, images, links, and lists.

View File

@@ -0,0 +1,9 @@
---
title: Basic HTML
superBlock: basic-html
certification: basic-html
---
## Introduction to Basic HTML
Intoduction to Basic HTML.

View File

@@ -0,0 +1,9 @@
---
title: Introduction to the Recipe Page
block: recipe-page
superBlock: basic-html
---
## Introduction to the Recipe Page
For this lab, you will create a web page of your favorite recipe.

View File

@@ -0,0 +1,9 @@
---
title: Introduction to the Build a Cat Blog Page
block: cat-blog-page
superBlock: semantic-html
---
## Introduction to the Build a Cat Blog Page
In this workshop, you will build an HTML only blog page using semantic elements including the <code>main</code>, <code>nav</code>, <code>article</code> and <code>footer</code> elements.

View File

@@ -0,0 +1,9 @@
---
title: Introduction to Event Hub
block: event-hub
superBlock: semantic-html
---
## Introduction to the Build an Event Hub
In this lab, you will build an event hub using semantic HTML.

View File

@@ -0,0 +1,9 @@
---
title: Semantic HTML
superBlock: semantic-html
certification: semantic-html
---
## Introduction to Semantic HTML
Intoduction to Semantic HTML.

View File

@@ -0,0 +1,30 @@
{
"name": "Build a Cat Blog Page",
"blockType": "workshop",
"blockLayout": "challenge-grid",
"isUpcomingChange": true,
"usesMultifileEditor": true,
"hasEditableBoundaries": true,
"dashedName": "cat-blog-page",
"superBlock": "semantic-html",
"order": 0,
"challengeOrder": [
{
"id": "669aff9f5488f1bea056416d",
"title": "Step 1"
},
{
"id": "669fc7e141e4703748c558bf",
"title": "Step 2"
},
{
"id": "669fc938d38e6e38ace9251e",
"title": "Step 3"
},
{
"id": "669fcb06c3034a39f5431a38",
"title": "Step 4"
}
],
"helpCategory": "HTML-CSS"
}

View File

@@ -0,0 +1,30 @@
{
"name": "Build a Cat Photo App",
"blockType": "workshop",
"blockLayout": "challenge-grid",
"isUpcomingChange": true,
"usesMultifileEditor": true,
"hasEditableBoundaries": true,
"dashedName": "cat-photo-app",
"superBlock": "basic-html",
"order": 0,
"challengeOrder": [
{
"id": "5dc174fcf86c76b9248c6eb2",
"title": "Step 1"
},
{
"id": "5dc1798ff86c76b9248c6eb3",
"title": "Step 2"
},
{
"id": "5dc17d3bf86c76b9248c6eb4",
"title": "Step 3"
},
{
"id": "5dc17dc8f86c76b9248c6eb5",
"title": "Step 4"
}
],
"helpCategory": "HTML-CSS"
}

View File

@@ -0,0 +1,12 @@
{
"name": "Build an Event Hub",
"blockType": "lab",
"blockLayout": "link",
"isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "event-hub",
"superBlock": "semantic-html",
"order": 1,
"challengeOrder": [{ "id": "66ebd4ae2812430bb883c787", "title": "Build an Event Hub" }],
"helpCategory": "HTML-CSS"
}

View File

@@ -0,0 +1,17 @@
{
"name": "Build a Recipe Page",
"blockType": "lab",
"blockLayout": "link",
"isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "recipe-page",
"superBlock": "basic-html",
"order": 1,
"challengeOrder": [
{
"id": "668f08ea07b99b1f4a91acab",
"title": "Build a Recipe Page"
}
],
"helpCategory": "HTML-CSS"
}

View File

@@ -0,0 +1,54 @@
---
id: 5dc174fcf86c76b9248c6eb2
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--
In this workshop, you will continue working with basic HTML elements like headings, paragraphs, and lists by building a cat photo app.
Begin the workshop by adding an `h1` element with the text of `CatPhotoApp`.
# --hints--
The text `CatPhotoApp` should be present in the code. You may want to check your spelling.
```js
assert.match(code, /catphotoapp/i);
```
Your `h1` element should have an opening tag. Opening tags have this syntax: `<elementName>`.
```js
assert.exists(document.querySelector('h1'));
```
Your `h1` element should have a closing tag. Closing tags have this syntax: `</elementName>`.
```js
assert.match(code, /<\/h1\>/);
```
Your `h1` element's text should be `CatPhotoApp`. You have either omitted the text, have a typo, or it is not between the `h1` element's opening and closing tags.
```js
assert.equal(document.querySelector('h1')?.innerText.toLowerCase(), 'catphotoapp');
```
# --seed--
## --seed-contents--
```html
<html>
<body>
--fcc-editable-region--
--fcc-editable-region--
</body>
</html>
```

View File

@@ -0,0 +1,80 @@
---
id: 5dc1798ff86c76b9248c6eb3
title: Step 2
challengeType: 0
dashedName: step-2
---
# --description--
Below the `h1` element, add an `h2` element with this text:
`Cat Photos`
# --hints--
Your `h1` element should have an opening tag. Opening tags have this syntax: `<elementName>`.
```js
assert.exists(document.querySelector('h1'));
```
Your `h1` element should have a closing tag. Closing tags have this syntax: `</elementName>`.
```js
assert.match(code, /<\/h1\>/);
```
You should only have one `h1` element. Remove the extra.
```js
assert.lengthOf(document.querySelectorAll('h1'), 1);
```
Your `h1` element's text should be 'CatPhotoApp'. You have either omitted the text or have a typo.
```js
assert.equal(document.querySelector('h1')?.innerText.toLowerCase(), 'catphotoapp');
```
Your `h2` element should have an opening tag. Opening tags have this syntax: `<elementName>`.
```js
assert.exists(document.querySelector('h2'));
```
Your `h2` element should have a closing tag. Closing tags have a `/` just after the `<` character.
```js
assert.match(code, /<\/h2\>/);
```
Your `h2` element's text should be `Cat Photos`. Only place the text `Cat Photos` between the opening and closing `h2` tags.
```js
assert.equal(document.querySelector('h2')?.innerText.toLowerCase(), 'cat photos');
```
Your `h2` element should be below the `h1` element. The `h1` element has greater importance and must be above the `h2` element.
```js
const collection = [...document.querySelectorAll('h1,h2')].map(
(node) => node.nodeName
);
assert.isBelow(collection.indexOf('H1'), collection.indexOf('H2'));
```
# --seed--
## --seed-contents--
```html
<html>
<body>
--fcc-editable-region--
<h1>CatPhotoApp</h1>
--fcc-editable-region--
</body>
</html>
```

View File

@@ -0,0 +1,60 @@
---
id: 5dc17d3bf86c76b9248c6eb4
title: Step 3
challengeType: 0
dashedName: step-3
---
# --description--
Create a `p` element below your `h2` element and give it the following text:
`Everyone loves cute cats online!`
# --hints--
Your `p` element should have an opening tag. Opening tags have the following syntax: `<elementName>`.
```js
assert.exists(document.querySelector('p'));
```
Your `p` element should have a closing tag. Closing tags have a `/` just after the `<` character.
```js
assert.match(code, /<\/p\>/);
```
Your `p` element's text should be `Everyone loves cute cats online!` You have either omitted the text or have a typo.
```js
const extraSpacesRemoved = document
.querySelector('p')
?.innerText.replace(/\s+/g, ' ');
assert.match(extraSpacesRemoved, /everyone loves cute cats online!$/i);
```
Your `p` element should be below the `h2` element. You have them in the wrong order.
```js
const collection = [...document.querySelectorAll('h2,p')].map(
(node) => node.nodeName
);
assert.isBelow(collection.indexOf('H2'), collection.indexOf('P'));
```
# --seed--
## --seed-contents--
```html
<html>
<body>
<h1>CatPhotoApp</h1>
--fcc-editable-region--
<h2>Cat Photos</h2>
--fcc-editable-region--
</body>
</html>
```

View File

@@ -0,0 +1,88 @@
---
id: 5dc17dc8f86c76b9248c6eb5
title: Step 4
challengeType: 0
dashedName: step-4
---
# --description--
Commenting allows you to leave messages without affecting the browser display. It also allows you to make code inactive. A comment in HTML starts with `<!--`, contains any number of lines of text, and ends with `-->`.
Here is an example of a comment with the `TODO: Remove h1`:
```html
<!-- TODO: Remove h1 -->
```
Add a comment above the `p` element with this text:
`TODO: Add link to cat photos`
# --hints--
Your comment should start with `<!--`. You are missing one or more of the characters that define the start of a comment.
```js
assert.match(code, /<!--/);
```
Your comment should end with `-->`. You are missing one or more of the characters that define the end of a comment.
```js
assert.match(code, /-->/);
```
Your code should not have extra opening/closing comment characters. You have an extra `<!--` or `-->` displaying in the browser.
```js
const noSpaces = code.replace(/\s/g, '');
assert.isBelow(noSpaces.match(/<!--/g)?.length, 2)
assert.isBelow(noSpaces.match(/-->/g)?.length, 2);
```
Your comment should be above the `p` element. You have them in the wrong order.
```js
assert.match(
code.replace(/\s/g, ''),
/<!--(.*?)--><p>everyonelovescutecatsonline!<\/p>/i
);
```
Your comment should contain the text `TODO: Add link to cat photos`.
```js
assert.match(code, /<!--\s*todo:\s+add\s+link\s+to\s+cat\s+photos\s*-->/i);
```
# --seed--
## --seed-contents--
```html
<html>
<body>
<h1>CatPhotoApp</h1>
<h2>Cat Photos</h2>
--fcc-editable-region--
<p>Everyone loves cute cats online!</p>
--fcc-editable-region--
</body>
</html>
```
# --solutions--
```html
<html>
<body>
<h1>CatPhotoApp</h1>
<h2>Cat Photos</h2>
<!-- TODO: Add link to cat photos -->
<p>Everyone loves cute cats online!</p>
</body>
</html>
```

View File

@@ -0,0 +1,224 @@
---
id: 668f08ea07b99b1f4a91acab
title: Build a Recipe Page
challengeType: 25
dashedName: build-a-recipe-page
demoType: onClick
---
# --description--
Fulfill the user stories below and get all the tests to pass to complete the lab.
**User Stories:**
1. You should have a `!DOCTYPE html` declaration.
1. You should have an `html` element with `lang` set to `en`.
1. You should have a `head` element containing a `title` element with the name of your recipe, and a `meta` element with a `charset` attribute set to `UTF-8`.
1. You should have a `body` element.
1. You should have an `h1` element with the name of your recipe.
1. You should have a `p` element that introduces the recipe below the `h1`.
1. You should have one `h2` element with the text `Ingredients` for the ingredients section.
1. You should have an unordered list (`ul` element) with at least four list items (`li` elements) that lists your ingredients below the first `h2` element.
1. You should have a second `h2` element with the text `Instructions` for the instructions section.
1. You should have an ordered list (`ol` element) with at least four list items that lists the recipe steps in order, below the second `h2`.
1. You should have one `img` element with a `src` attribute set to a valid image, you can use `https://cdn.freecodecamp.org/curriculum/labs/recipe.jpg` if you would like, and an `alt` attribute describing the image.
# --hints--
Your recipe page should have a `!DOCTYPE html` declaration.
```js
assert.match(code, /<!DOCTYPE html>/i);
```
You should have an `html` element with `lang` set to `en`.
```js
assert.match(code, /<html\s+lang\s*=\s*('|")en\1\s*>[\s\S]*<\/\s*html\s*>/gi);
```
You should have a `head` element within the `html` element.
```js
assert.match(code, /<html[\s\S]*>[\s\S]*<\s*head\s*>[\s\S]*<\/\s*head\s*>[\s\S]*<\/\s*html\s*>/i);
```
You should have `title` element within your `head` element.
```js
assert.match(code, /<\s*head\s*>[\s\S]*<\s*title\s*>[\s\S]*<\/\s*title\s*>[\s\S]*<\/\s*head\s*>/i);
```
Your `title` element should have your recipe title.
```js
assert.isAbove(document.querySelector('title')?.innerText.trim().length, 0);
```
You should have a `meta` element within your `head` element.
```js
assert.match(code, /<\s*head\s*>[\s\S]*<\s*meta[\s\S]*>[\s\S]*<\/\s*head\s*>/i);
```
Your `meta` element should have its `charset` attribute set to `UTF-8`.
```js
assert.match(code, /<\s*meta[\s\S]+?charset\s*=\s*('|")UTF-8\1/i);
```
You should have a `body` element within your `html` element.
```js
assert.match(code, /<\s*html[\s\S]*>[\s\S]*<\s*head\s*>[\s\S]*<\/\s*head\s*>[\s\S]*<\s*body\s*>[\s\S]*<\/\s*body\s*>[\s\S]*<\/\s*html\s*>/i);
```
You should have an `h1` element with the name of your recipe.
```js
assert.isAbove(document.querySelector('h1')?.innerText.length, 0);
```
You should only have one `h1` element.
```js
assert.lengthOf(document.querySelectorAll('h1'), 1);
```
You should have a `p` element below your `h1` element.
```js
assert.strictEqual(document.querySelector('h1')?.nextElementSibling, document.querySelector('p'));
```
Your first `p` element should describe your recipe.
```js
assert.isNotEmpty(document.querySelector('p')?.textContent?.trim());
```
Your first `h2` element should have the text `Ingredients`.
```js
assert.equal(document.querySelectorAll('h2')[0]?.innerText, 'Ingredients');
```
You should have an unordered list element below your first `h2` element.
```js
assert.strictEqual(document.querySelector('ul')?.previousElementSibling.tagName, 'H2');
```
You should have at least four list item elements in your unordered list with the ingredients.
```js
const els = document.querySelectorAll('ul > li');
assert.isAbove(els.length, 3);
els.forEach(el => assert.isAbove(el.innerText.trim().length, 0))
```
Your second `h2` element should have the text `Instructions`.
```js
assert.equal(document.querySelectorAll('h2')[1]?.innerText, 'Instructions');
```
You should have an ordered list element below your second `h2` element.
```js
assert.strictEqual(document.querySelectorAll('h2')?.[1]?.nextElementSibling?.tagName, "OL");
```
You should have at least four list item elements in your ordered list with the instructions.
```js
const els = document.querySelectorAll('ol > li');
assert.isAbove(els.length, 3);
els.forEach(el => assert.isAbove(el.innerText.trim().length, 0))
```
You should have at least one `img` element.
```js
assert.exists(document.querySelector('img'));
```
All your `img` elements should have a valid `src` attribute and value.
```js
const img = document.querySelector('img');
const rawSrc = img?.getAttribute('src');
const resolvedSrc = img?.src;
const re = new RegExp(window.location.href, "ig");
assert.isAbove(rawSrc?.trim().length, 0, "The 'src' attribute must be explicitly set.");
assert.notMatch(resolvedSrc, re, "The 'src' should not start with the current page URL");
img.onload = () => {
console.log('Image loaded successfully.');
};
img.onerror = (error) => {
console.error('Image failed to load:', error);
assert.fail("Your image's URL should be valid."); // Make the test instafail
};
if (img.complete) {
img.onload && img.onload();
};
```
All your `img` elements should have an `alt` attribute to describe the image.
```js
assert.isAbove(document.querySelector('img')?.alt.length, 0);
```
# --seed--
## --seed-contents--
```html
```
# --solutions--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Chocolate chip cookies recipe</title>
</head>
<body>
<h1>Chocolate Chip Cookies</h1>
<p>Welcome to the ultimate guide for making mini chocolate chip cookies! These bite-sized treats are perfect for
satisfying your sweet tooth without overindulging. Follow this simple recipe to create delicious,
crispy-on-the-outside, chewy-on-the-inside mini chocolate chip cookies that everyone will love.</p>
<img src="https://cdn.freecodecamp.org/curriculum/labs/recipe.jpg" alt="Ingredients for baking: three eggs, a bowl of flour, a glass of milk, and a whisk arranged on a wooden table.">
<h2>Ingredients</h2>
<ul>
<li>1 cup all-purpose flour</li>
<li>1/2 teaspoon baking soda</li>
<li>1/4 cup unsalted butter, softened</li>
<li>1/4 cup granulated sugar</li>
<li>1/2 teaspoon vanilla extract</li>
<li>1/2 cup mini chocolate chips</li>
</ul>
<h2>Instructions</h2>
<ol>
<li>Preheat your oven to 350°F (175°C) and line a baking sheet with parchment paper.</li>
<li>In a bowl, whisk together the flour and baking soda.</li>
<li>In another bowl, beat the butter, sugar, and vanilla extract until creamy.</li>
<li>Gradually add the dry ingredients to the wet mixture, then fold in the mini chocolate chips.</li>
<li>Drop small spoonfuls of dough onto the baking sheet.</li>
<li>Bake for 8-10 minutes, then let cool before enjoying!</li>
</ol>
</body>
</html>
```

View File

@@ -0,0 +1,58 @@
---
id: 669aff9f5488f1bea056416d
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--
In this workshop, you will practice working with semantic HTML by building a blog page dedicated to Mr. Whiskers the cat.
To begin the project, add the `<!DOCTYPE html>`, and an `html` element with a `lang` attribute of `en`.
Remember that you learned how to build a basic HTML boilerplate like this in the previous module.
```html
<!DOCTYPE html>
<html lang="en">
<!--all other elements go here-->
</html>
```
# --hints--
You should have the `<!DOCTYPE html>`.
```js
assert.match(code, /<!DOCTYPE\s+html>/i);
```
You should have an opening `html` tag with the language set to english.
```js
assert.match(code, /<html\s+lang\s*=\s*('|")en\1\s*>/gi);
```
You should have a closing `html` tag.
```js
assert.match(code, /<\/html>/i);
```
Your `DOCTYPE` should come before the `html` element.
```js
assert.match(code, /<!DOCTYPE\s+html>[.\n\s]*<html\s+lang\s*=\s*('|")en\1\s*>/im)
```
# --seed--
## --seed-contents--
```html
--fcc-editable-region--
--fcc-editable-region--
```

View File

@@ -0,0 +1,43 @@
---
id: 669fc7e141e4703748c558bf
title: Step 2
challengeType: 0
dashedName: step-2
---
# --description--
Inside the `html` element, add a `head` element.
# --hints--
You should have an opening `head` tag.
```js
assert.match(code, /<head>/i);
```
You should have a closing `head` tag.
```js
assert.match(code, /<\/head>/i);
```
Your opening `head` tag should come before the closing `head` tag.
```js
assert.match(code, /<head>[.\n\s]*<\/head>/im)
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
--fcc-editable-region--
--fcc-editable-region--
</html>
```

View File

@@ -0,0 +1,87 @@
---
id: 669fc938d38e6e38ace9251e
title: Step 3
challengeType: 0
dashedName: step-3
---
# --description--
Inside your `head` element, nest a `meta` element with the `charset` attribute set to the value `"UTF-8"`.
Below that `meta` element, add a `title` element.
The `title` element's text should be `Mr. Whiskers' Blog`.
# --hints--
You should have a `meta` element.
```js
assert.isNotNull(document.querySelector("meta"));
```
The `meta` element is a void element, it should not have an end tag `</meta>`.
```js
assert.notMatch(code, /<\/meta>/i);
```
Your `meta` tag should have a `charset` attribute.
```js
assert.match(code, /<meta\s+charset\s*/i);
```
Your `charset` attribute should have a value of `"UTF-8"`.
```js
assert.match(code, /charset\s*=\s*('|")UTF-8\1/i);
```
Your `meta` element should be nested inside your `head` element.
```js
const meta = document.querySelector('head > meta');
assert.strictEqual(meta?.parentElement?.tagName, 'HEAD');
```
You should have an opening `title` tag.
```js
assert.match(code, /<title>/i);
```
You should have a closing `title` tag.
```js
assert.match(code, /<\/title>/i);
```
Your `title` element should be nested in your `head` element.
```js
assert.match(code, /<head>.*\s*<title>.*<\/title>.*\s*<\/head>/si);
```
Your `title` element should have the text `Mr. Whiskers' Blog`. You may need to check your spelling.
```js
const titleText = document.querySelector('title')?.innerText
assert.strictEqual(titleText, "Mr. Whiskers' Blog");
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
--fcc-editable-region--
<head>
</head>
--fcc-editable-region--
</html>
```

View File

@@ -0,0 +1,69 @@
---
id: 669fcb06c3034a39f5431a38
title: Step 4
challengeType: 0
dashedName: step-4
---
# --description--
To prepare creating some actual content, add a `body` element below the `head` element.
# --hints--
You should have an opening `<body>` tag.
```js
assert.match(code, /<body>/i);
```
You should have a closing `</body>` tag.
```js
assert.match(code, /<\/body>/i);
```
You should not change your `head` element. Make sure you did not delete your closing tag.
```js
assert.match(code, /<head>/i);
assert.match(code, /<\/head>/i);
```
Your `body` element should come after your `head` element.
```js
assert.match(code, /<\/head>[.\n\s]*<body>/im)
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
--fcc-editable-region--
<head>
<title>Mr. Whiskers' Blog</title>
<meta charset="UTF-8" />
</head>
--fcc-editable-region--
</html>
```
# --solutions--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Mr. Whiskers' Blog</title>
<meta charset="UTF-8" />
</head>
<body></body>
</html>
```

View File

@@ -0,0 +1,335 @@
---
id: 66ebd4ae2812430bb883c787
title: Build an Event Hub
challengeType: 25
dashedName: lab-event-hub
demoType: onClick
---
# --description--
In this lab you will utilize the semantic HTML elements to create the structure of a web page. You'll add content and images to make it look like a real event hub.
Fulfill the user stories below and get all the tests to pass to complete the lab.
**User Stories:**
1. You should have a `header` element.
1. Inside the `header` element, you should have an `h1` element that contains the text `Event Hub`, and a `nav` element.
1. Inside the `nav` element, you should have an unordered list of two items containing links to different sections of the page. The first item should have the text `Upcoming Events`, and the second item should have the text `Past Events`.
1. Each link should be represented by an `a` element with an `href` attribute that links to the corresponding section of the page, `#upcoming-events` and `#past-events` respectively.
1. You should have a `main` element that contains the different sections of the page.
1. Inside the `main` element, you should have two `section` elements.
1. The first `section` element should have an `id` attribute with the value `upcoming-events`
1. Inside the `#upcoming-events` section, you should have:
- An `h2` element with the text `Upcoming Events`.
- Two `article` elements. Each article should represent an event, and it should have :
- An `h3` element for the event title.
- A `p` element for the event description. You can add a date at the bottom if you like.
1. The second `section` element should have an `id` attribute with the value `past-events`.
1. Inside the `#past-events` section, you should have:
- An `h2` element with the text `Past Events`.
- Two `article` elements. Each article element should represent a past event, and it should have:
- An `h3` element for the event title,
- A `p` element for the event description. You can add a date at the bottom if you like.
- An image element with the `src` attribute pointing to an image file and the `alt` attribute with a description of the image.
**Note:** You can use any text for the event descriptions and dates. You can use the following image URLs for the images if you like:
- `https://cdn.freecodecamp.org/curriculum/labs/past-event1.jpg`.
- `https://cdn.freecodecamp.org/curriculum/labs/past-event2.jpg`.
# --hints--
You should have a `header` element.
```js
assert.isNotNull(document.querySelector("header"));
```
Your `header` element should come after the opening `body` tag.
```js
assert.equal(document.querySelector("body")?.firstElementChild?.tagName, "HEADER");
```
Inside the `header` element, you should have an `h1` element that contains the text `Event Hub`.
```js
const h1Element = document.querySelector('header h1');
assert.strictEqual(h1Element?.innerText, "Event Hub");
```
Inside the `header` element, after the `h1` element, you should have a `nav` element.
```js
assert.isNotNull(document.querySelector("header>h1+nav"));
```
Your `nav` element should contain an unordered list of two items.
```js
const liElements = document.querySelectorAll('header nav>ul>li');
assert.isNotNull('header nav>ul');
assert.strictEqual(liElements.length, 2);
```
The first item in the unordered list should be a link.
```js
const firstLink = document.querySelectorAll('header nav ul li a')[0];
assert.exists(firstLink);
```
The second item in the unordered list should be a link.
```js
const secondLink = document.querySelectorAll('header nav ul li a')[1];
assert.exists(secondLink);
```
The text of the first item in the unordered list should be `Upcoming Events`.
```js
const firstLink = document.querySelectorAll('header nav>ul>li>a')[0];
assert.strictEqual(firstLink.innerText, "Upcoming Events");
```
The first item in the unordered list should have the `href` set to `#upcoming-events`.
```js
const anchorElement = document.querySelectorAll("header nav>ul>li>a")[0];
const hrefAttribute = anchorElement?.getAttribute("href");
assert.strictEqual(hrefAttribute, "#upcoming-events");
```
The text of the second item in the unordered list should be `Past Events`.
```js
const secondLink = document.querySelectorAll('header nav>ul>li>a')[1];
assert.strictEqual(secondLink.innerText, "Past Events");
```
The second item in the unordered list should have the `href` set to `#past-events`.
```js
const anchorElement = document.querySelectorAll("header nav>ul>li>a")[1];
const hrefAttribute = anchorElement?.getAttribute("href");
assert.strictEqual(hrefAttribute, "#past-events");
```
You should have a `main` element after the `header` element closing tag.
```js
const mainElement = document.querySelector("body>header+main");
assert.isNotNull(mainElement);
```
Inside the `main` element, you should have two `section` elements.
```js
const sectionElements = document.querySelectorAll('body>header+main>section');
assert.strictEqual(sectionElements.length, 2);
```
Your first `section` element should have an `id` attribute with the value `upcoming-events`.
```js
const firstSection = document.querySelectorAll('body>header+main>section')[0];
const idAttribute = firstSection?.getAttribute("id");
assert.strictEqual(idAttribute, "upcoming-events");
```
Your second `section` element should have an `id` attribute with the value `past-events`.
```js
const secondSection = document.querySelectorAll('body>header+main>section')[1];
const idAttribute = secondSection?.getAttribute("id");
assert.strictEqual(idAttribute, "past-events");
```
Inside the `#upcoming-events` section, you should have an `h2` element with the text `Upcoming Events`.
```js
const h2Element = document.querySelector('#upcoming-events h2');
assert.strictEqual(h2Element?.innerText, "Upcoming Events");
```
Inside the `#upcoming-events` section, you should have two `article` elements below the `h2` element.
```js
const articleElements = document.querySelectorAll('#upcoming-events h2 ~ article');
assert.strictEqual(articleElements.length, 2);
```
Both of the `article` elements inside the `#upcoming-events` section should have an `h3` element for the event title.
```js
const h3Elements = document.querySelectorAll('#upcoming-events article h3');
assert.strictEqual(h3Elements.length, 2);
```
Both of the `article` elements inside the `#upcoming-events` section should have a paragraph element for the event description.
```js
const articles = document.querySelectorAll('#upcoming-events article');
assert.isNotEmpty(articles);
articles.forEach(article => {
assert.isAtLeast(article.querySelectorAll('h3 ~ p').length, 1);
});
```
Inside the `#past-events` section, you should have an `h2` element with the text `Past Events`.
```js
const h2Element = document.querySelector('#past-events h2');
assert.strictEqual(h2Element?.innerText, "Past Events");
```
Inside the `#past-events` section, you should have two `article` elements below the `h2` element.
```js
const articleElements = document.querySelectorAll('#past-events h2 ~ article');
assert.strictEqual(articleElements.length, 2);
```
Both of the `article` elements inside the `#past-events` section should have an `h3` element for the event title.
```js
const h3Elements = document.querySelectorAll('#past-events article h3');
assert.strictEqual(h3Elements.length, 2);
```
Both of the `article` elements inside the `#past-events` section should have a paragraph element for the event description.
```js
const articles = document.querySelectorAll('#past-events article');
assert.isNotEmpty(articles);
articles.forEach(article => {
assert.isAtLeast(article.querySelectorAll('h3 ~ p').length, 1);
});
```
Both of the `article` elements inside the `#past-events` section should have an image element.
```js
const imgElements = document.querySelectorAll('#past-events article img');
assert.strictEqual(imgElements.length, 2);
```
Both of the image elements inside the `#past-events` section should have the `src` attribute pointing to an image file.
```js
const imgElements = document.querySelectorAll('#past-events article img');
assert.strictEqual(imgElements.length, 2);
for (let img of imgElements) {
assert.exists(img.getAttribute("src"));
}
```
Both of the image elements inside the `#past-events` section should have the `alt` attribute with a description of the image.
```js
const imgElements = document.querySelectorAll('#past-events article img');
assert.strictEqual(imgElements.length, 2);
for (let img of imgElements) {
assert.exists(img.getAttribute("alt"));
}
```
Each `h3` element should have the event title.
```js
const eventTitles = document.querySelectorAll('h3');
assert.isNotEmpty(eventTitles);
eventTitles.forEach((eventTitle => assert.isNotEmpty(eventTitle.innerText)));
```
Each `p` element should have the event description.
```js
const eventDescriptions = document.querySelectorAll('p');
assert.isNotEmpty(eventDescriptions);
eventDescriptions.forEach((eventDescription => assert.isNotEmpty(eventDescription.innerText)));
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Hub</title>
</head>
<body>
</body>
</html>
```
# --solutions--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event Hub</title>
</head>
<body>
<header>
<h1>Event Hub</h1>
<nav>
<ul>
<li><a href="#upcoming-events">Upcoming Events</a></li>
<li><a href="#past-events">Past Events</a></li>
</ul>
</nav>
</header>
<main>
<section id="upcoming-events">
<h2>Upcoming Events</h2>
<article>
<h3>AI & Machine Learning Conference 2024</h3>
<p>Join us for a deep dive into the latest advancements in artificial intelligence and machine learning. Industry leaders will share insights and case studies on how AI is transforming various sectors.</p>
<p>Date: August 10, 2024</p>
</article>
<article>
<h3>Web Development Bootcamp</h3>
<p>A hands-on workshop designed for developers looking to enhance their skills in modern web technologies including React, Node.js, and GraphQL. Perfect for both beginners and experienced developers.</p>
<p>Date: September 5, 2024</p>
</article>
</section>
<section id="past-events">
<h2>Past Events</h2>
<article>
<h3>Cybersecurity Summit 2024</h3>
<p>An event focusing on the latest trends and threats in cybersecurity. Experts discussed strategies for protecting data and ensuring privacy in an increasingly digital world.</p>
<p>Date: June 15, 2024</p>
<img src="https://cdn.freecodecamp.org/curriculum/labs/past-event1.jpg" alt="Image from Cybersecurity Summit 2024">
</article>
<article>
<h3>Blockchain Expo 2024</h3>
<p>A comprehensive event covering the future of blockchain technology. Topics included decentralized finance (DeFi), smart contracts, and the impact of blockchain on various industries.</p>
<p>Date: July 20, 2024</p>
<img src="https://cdn.freecodecamp.org/curriculum/labs/past-event2.jpg" alt="Image from Blockchain Expo 2024">
</article>
</section>
</main>
</body>
</html>
```

View File

@@ -2,7 +2,10 @@ const Joi = require('joi');
Joi.objectId = require('joi-objectid')(Joi);
const { challengeTypes } = require('../../shared/config/challenge-types');
const { chapterBasedSuperBlocks } = require('../../shared/config/curriculum');
const {
chapterBasedSuperBlocks,
catalogSuperBlocks
} = require('../../shared/config/curriculum');
const {
availableCharacters,
availableBackgrounds,
@@ -128,7 +131,7 @@ const schema = Joi.object()
block: Joi.string().regex(slugRE).required(),
blockId: Joi.objectId(),
blockType: Joi.when('superBlock', {
is: chapterBasedSuperBlocks,
is: [...chapterBasedSuperBlocks, ...catalogSuperBlocks],
then: Joi.valid(
'workshop',
'lab',

View File

@@ -52,6 +52,18 @@ const duplicatedProjectIds = [
'5ef9b03c81a63668521804ee',
'62bb4009e3458a128ff57d5d',
// Recipe Page
'668f08ea07b99b1f4a91acab',
// Cat Blog
'669aff9f5488f1bea056416d',
'669fc7e141e4703748c558bf',
'669fc938d38e6e38ace9251e',
'669fcb06c3034a39f5431a38',
// Event hub
'66ebd4ae2812430bb883c787',
// Survey Form
'587d78af367417b2b2512b03',

View File

@@ -178,7 +178,7 @@ describe('getSuperBlockFromPath', () => {
.filter(item => fs.lstatSync(path.join(englishFolder, item)).isDirectory());
it('handles all the directories in ./challenges/english', () => {
expect.assertions(27);
expect.assertions(29);
for (const directory of directories) {
expect(() => getSuperBlockFromDir(directory)).not.toThrow();
@@ -186,7 +186,7 @@ describe('getSuperBlockFromPath', () => {
});
it("returns valid superblocks (or 'certifications') for all valid arguments", () => {
expect.assertions(27);
expect.assertions(29);
const superBlockPaths = directories.filter(x => x !== '00-certifications');

View File

@@ -0,0 +1,10 @@
import { catalogSuperBlocks } from './curriculum';
import { catalog } from './catalog';
describe('catalog', () => {
it('should have exactly one entry for each superblock in SuperBlockStage.Catalog', () => {
expect(catalog.map(course => course.superBlock.toString()).sort()).toEqual(
catalogSuperBlocks.map(sb => sb.toString()).sort()
);
});
});

26
shared/config/catalog.ts Normal file
View File

@@ -0,0 +1,26 @@
import { SuperBlocks } from './curriculum';
enum Levels {
Beginner = 'beginner',
Intermediate = 'intermediate',
Advanced = 'advanced'
}
interface Catalog {
superBlock: SuperBlocks;
level: Levels;
hours: number;
}
export const catalog: Catalog[] = [
{
superBlock: SuperBlocks.BasicHtml,
level: Levels.Beginner,
hours: 2
},
{
superBlock: SuperBlocks.SemanticHtml,
level: Levels.Beginner,
hours: 2
}
];

View File

@@ -281,6 +281,8 @@ export const superBlockToCertMap: {
[SuperBlocks.ProjectEuler]: null,
[SuperBlocks.TheOdinProject]: null,
[SuperBlocks.RosettaCode]: null,
[SuperBlocks.BasicHtml]: null,
[SuperBlocks.SemanticHtml]: null,
[SuperBlocks.DevPlayground]: null
};

View File

@@ -74,6 +74,7 @@ export const blocklistedUsernames = [
'backend-challenge-completed',
'blocked',
'bonfire',
'catalog',
'cats.json',
'challenge-completed',
'challenge',

View File

@@ -33,6 +33,7 @@ describe('generateSuperBlockList', () => {
});
const tempSuperBlockMap = { ...superBlockStages };
tempSuperBlockMap[SuperBlockStage.Upcoming] = [];
tempSuperBlockMap[SuperBlockStage.Catalog] = [];
expect(result).toHaveLength(Object.values(tempSuperBlockMap).flat().length);
});
});

View File

@@ -29,6 +29,8 @@ export enum SuperBlocks {
A2Chinese = 'a2-professional-chinese',
RosettaCode = 'rosetta-code',
PythonForEverybody = 'python-for-everybody',
BasicHtml = 'basic-html',
SemanticHtml = 'semantic-html',
DevPlayground = 'dev-playground'
}
@@ -61,6 +63,8 @@ export const superBlockToFolderMap = {
[SuperBlocks.FullStackDeveloper]: '25-front-end-development',
[SuperBlocks.A2Spanish]: '26-a2-professional-spanish',
[SuperBlocks.A2Chinese]: '27-a2-professional-chinese',
[SuperBlocks.BasicHtml]: '28-basic-html',
[SuperBlocks.SemanticHtml]: '29-semantic-html',
[SuperBlocks.DevPlayground]: '99-dev-playground'
};
@@ -91,7 +95,8 @@ export enum SuperBlockStage {
Extra,
Legacy,
Upcoming,
Next
Next,
Catalog
}
const defaultStageOrder = [
@@ -108,7 +113,9 @@ export function getStageOrder({
}: Config): SuperBlockStage[] {
const stageOrder = [...defaultStageOrder];
if (showUpcomingChanges) stageOrder.push(SuperBlockStage.Upcoming);
if (showUpcomingChanges) {
stageOrder.push(SuperBlockStage.Upcoming, SuperBlockStage.Catalog);
}
return stageOrder;
}
@@ -119,7 +126,6 @@ export type StageMap = {
// Groups of superblocks in learn map. This should include all superblocks.
export const superBlockStages: StageMap = {
[SuperBlockStage.Core]: [SuperBlocks.FullStackDeveloper],
[SuperBlockStage.English]: [SuperBlocks.A2English, SuperBlocks.B1English],
[SuperBlockStage.Professional]: [SuperBlocks.FoundationalCSharp],
[SuperBlockStage.Extra]: [
@@ -150,11 +156,16 @@ export const superBlockStages: StageMap = {
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.DevPlayground
]
],
// Catalog is treated like upcoming for now
// Add catalog superBlocks to catalog.ts when adding new superBlocks
[SuperBlockStage.Catalog]: [SuperBlocks.BasicHtml, SuperBlocks.SemanticHtml]
};
Object.freeze(superBlockStages);
export const catalogSuperBlocks = superBlockStages[SuperBlockStage.Catalog];
type NotAuditedSuperBlocks = {
[key in Languages]: SuperBlocks[];
};
@@ -178,6 +189,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.PythonForEverybody,
SuperBlocks.BasicHtml,
SuperBlocks.SemanticHtml,
SuperBlocks.DevPlayground
],
[Languages.Chinese]: [
@@ -190,6 +203,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.PythonForEverybody,
SuperBlocks.BasicHtml,
SuperBlocks.SemanticHtml,
SuperBlocks.DevPlayground
],
[Languages.ChineseTraditional]: [
@@ -202,6 +217,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.PythonForEverybody,
SuperBlocks.BasicHtml,
SuperBlocks.SemanticHtml,
SuperBlocks.DevPlayground
],
[Languages.Italian]: [
@@ -214,6 +231,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.PythonForEverybody,
SuperBlocks.BasicHtml,
SuperBlocks.SemanticHtml,
SuperBlocks.DevPlayground
],
[Languages.Portuguese]: [
@@ -224,6 +243,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.PythonForEverybody,
SuperBlocks.BasicHtml,
SuperBlocks.SemanticHtml,
SuperBlocks.DevPlayground
],
[Languages.Ukrainian]: [
@@ -233,6 +254,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.B1English,
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.BasicHtml,
SuperBlocks.SemanticHtml,
SuperBlocks.DevPlayground
],
[Languages.Japanese]: [
@@ -243,6 +266,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.B1English,
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.BasicHtml,
SuperBlocks.SemanticHtml,
SuperBlocks.DevPlayground
],
[Languages.German]: [
@@ -262,6 +287,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.PythonForEverybody,
SuperBlocks.BasicHtml,
SuperBlocks.SemanticHtml,
SuperBlocks.DevPlayground
],
[Languages.Swahili]: [
@@ -288,6 +315,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.PythonForEverybody,
SuperBlocks.BasicHtml,
SuperBlocks.SemanticHtml,
SuperBlocks.DevPlayground
],
[Languages.Korean]: [
@@ -315,6 +344,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.DataVis,
SuperBlocks.RelationalDb,
SuperBlocks.RosettaCode,
SuperBlocks.BasicHtml,
SuperBlocks.SemanticHtml,
SuperBlocks.DevPlayground
]
};

View File

@@ -98,5 +98,13 @@ export const superBlockList = [
{
name: 'A2 Professional Chinese (Beta)',
path: '27-a2-professional-chinese'
},
{
name: 'Basic HTML',
path: '28-basic-html'
},
{
name: 'Semantic HTML',
path: '29-semantic-html'
}
];

View File

@@ -141,7 +141,9 @@ ${result.error.message}`);
.filter(([key]) => {
const stage = Number(key) as SuperBlockStage;
return (
stage !== SuperBlockStage.Next && stage !== SuperBlockStage.Upcoming
stage !== SuperBlockStage.Next &&
stage !== SuperBlockStage.Upcoming &&
stage !== SuperBlockStage.Catalog
);
})
.flatMap(([, superBlocks]) => superBlocks);

View File

@@ -58,7 +58,9 @@ describe('external curriculum data build', () => {
test('the available-superblocks file should have the correct structure', async () => {
const filteredSuperBlockStages: string[] = Object.keys(SuperBlockStage)
.filter(key => isNaN(Number(key))) // Filter out numeric keys to get only the names
.filter(name => name !== 'Upcoming' && name !== 'Next') // Filter out 'Upcoming' and 'Next'
.filter(
name => name !== 'Upcoming' && name !== 'Next' && name !== 'Catalog'
) // Filter out 'Upcoming', 'Next', and 'Catalog'
.map(name => name.toLowerCase());
const validateAvailableSuperBlocks = availableSuperBlocksValidator();