diff --git a/client/i18n/locales.test.ts b/client/i18n/locales.test.ts
index 289b0ca554b..622c4f23d0c 100644
--- a/client/i18n/locales.test.ts
+++ b/client/i18n/locales.test.ts
@@ -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);
diff --git a/client/i18n/locales/english/intro.json b/client/i18n/locales/english/intro.json
index eb1e256cb1e..b48d2f7d4e5 100644
--- a/client/i18n/locales/english/intro.json
+++ b/client/i18n/locales/english/intro.json
@@ -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 main, nav, article and footer elements."
+ ]
+ },
+ "event-hub": {
+ "title": "Build an Event Hub",
+ "intro": [
+ "In this lab, you'll build an event hub and review semantic elements like header, nav, article, and more."
+ ]
+ }
+ }
+ },
"dev-playground": {
"title": "Dev Playground",
"intro": ["Playground for creating and testing challenges"],
diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json
index 5b7c2edd728..b5fca7e722b 100644
--- a/client/i18n/locales/english/translations.json
+++ b/client/i18n/locales/english/translations.json
@@ -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"
+ }
+ }
}
}
diff --git a/client/src/assets/superblock-icon.tsx b/client/src/assets/superblock-icon.tsx
index 8e50319efbf..5a5c6a58e81 100644
--- a/client/src/assets/superblock-icon.tsx
+++ b/client/src/assets/superblock-icon.tsx
@@ -47,6 +47,8 @@ const iconMap = {
[SuperBlocks.A2Chinese]: A2EnglishIcon,
[SuperBlocks.RosettaCode]: RosettaCodeIcon,
[SuperBlocks.PythonForEverybody]: PythonIcon,
+ [SuperBlocks.BasicHtml]: Code,
+ [SuperBlocks.SemanticHtml]: Code,
[SuperBlocks.DevPlayground]: Code
};
diff --git a/client/src/components/Map/index.tsx b/client/src/components/Map/index.tsx
index 5cdb67e0f47..ea0ccee64cc 100644
--- a/client/src/components/Map/index.tsx
+++ b/client/src/components/Map/index.tsx
@@ -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(
diff --git a/client/src/pages/catalog.css b/client/src/pages/catalog.css
new file mode 100644
index 00000000000..df6ecc5c893
--- /dev/null
+++ b/client/src/pages/catalog.css
@@ -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;
+}
diff --git a/client/src/pages/catalog.tsx b/client/src/pages/catalog.tsx
new file mode 100644
index 00000000000..db53b5fab48
--- /dev/null
+++ b/client/src/pages/catalog.tsx
@@ -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 ? (
+
+
+ {t('curriculum.catalog.title')}
+
+
+
+ {catalog.map(course => {
+ const { superBlock, level, hours } = course;
+
+ const { title, summary } = t(`intro:${superBlock}`, {
+ returnObjects: true
+ }) as {
+ title: string;
+ summary: string[];
+ };
+
+ return (
+
+
+
{title}
+
+ {summary.map(text => (
+
{text}
+ ))}
+
+
+
+ {t(`curriculum.catalog.levels.${level}`)} • {hours}{' '}
+ hours
+
+
+ {t('buttons.go-to-course')}
+
+
+
+ );
+ })}
+
+
+
+
+ ) : (
+
+ );
+};
+
+export default CatalogPage;
diff --git a/client/src/pages/learn/basic-html/cat-photo-app/index.md b/client/src/pages/learn/basic-html/cat-photo-app/index.md
new file mode 100644
index 00000000000..577863bcd62
--- /dev/null
+++ b/client/src/pages/learn/basic-html/cat-photo-app/index.md
@@ -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.
diff --git a/client/src/pages/learn/basic-html/index.md b/client/src/pages/learn/basic-html/index.md
new file mode 100644
index 00000000000..a9d78dd3d35
--- /dev/null
+++ b/client/src/pages/learn/basic-html/index.md
@@ -0,0 +1,9 @@
+---
+title: Basic HTML
+superBlock: basic-html
+certification: basic-html
+---
+
+## Introduction to Basic HTML
+
+Intoduction to Basic HTML.
diff --git a/client/src/pages/learn/basic-html/recipe-page/index.md b/client/src/pages/learn/basic-html/recipe-page/index.md
new file mode 100644
index 00000000000..8d55b3511ff
--- /dev/null
+++ b/client/src/pages/learn/basic-html/recipe-page/index.md
@@ -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.
diff --git a/client/src/pages/learn/semantic-html/cat-blog-page/index.md b/client/src/pages/learn/semantic-html/cat-blog-page/index.md
new file mode 100644
index 00000000000..78ef580e147
--- /dev/null
+++ b/client/src/pages/learn/semantic-html/cat-blog-page/index.md
@@ -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 main, nav, article and footer elements.
diff --git a/client/src/pages/learn/semantic-html/event-hub/index.md b/client/src/pages/learn/semantic-html/event-hub/index.md
new file mode 100644
index 00000000000..8fb63310da4
--- /dev/null
+++ b/client/src/pages/learn/semantic-html/event-hub/index.md
@@ -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.
diff --git a/client/src/pages/learn/semantic-html/index.md b/client/src/pages/learn/semantic-html/index.md
new file mode 100644
index 00000000000..a08db1766d7
--- /dev/null
+++ b/client/src/pages/learn/semantic-html/index.md
@@ -0,0 +1,9 @@
+---
+title: Semantic HTML
+superBlock: semantic-html
+certification: semantic-html
+---
+
+## Introduction to Semantic HTML
+
+Intoduction to Semantic HTML.
diff --git a/curriculum/challenges/_meta/cat-blog-page/meta.json b/curriculum/challenges/_meta/cat-blog-page/meta.json
new file mode 100644
index 00000000000..a22d3ff0acb
--- /dev/null
+++ b/curriculum/challenges/_meta/cat-blog-page/meta.json
@@ -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"
+}
diff --git a/curriculum/challenges/_meta/cat-photo-app/meta.json b/curriculum/challenges/_meta/cat-photo-app/meta.json
new file mode 100644
index 00000000000..56cee9e1268
--- /dev/null
+++ b/curriculum/challenges/_meta/cat-photo-app/meta.json
@@ -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"
+}
diff --git a/curriculum/challenges/_meta/event-hub/meta.json b/curriculum/challenges/_meta/event-hub/meta.json
new file mode 100644
index 00000000000..56f7ea3b39f
--- /dev/null
+++ b/curriculum/challenges/_meta/event-hub/meta.json
@@ -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"
+}
diff --git a/curriculum/challenges/_meta/recipe-page/meta.json b/curriculum/challenges/_meta/recipe-page/meta.json
new file mode 100644
index 00000000000..0758bdace48
--- /dev/null
+++ b/curriculum/challenges/_meta/recipe-page/meta.json
@@ -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"
+}
diff --git a/curriculum/challenges/english/28-basic-html/cat-photo-app/5dc174fcf86c76b9248c6eb2.md b/curriculum/challenges/english/28-basic-html/cat-photo-app/5dc174fcf86c76b9248c6eb2.md
new file mode 100644
index 00000000000..6986a0647f0
--- /dev/null
+++ b/curriculum/challenges/english/28-basic-html/cat-photo-app/5dc174fcf86c76b9248c6eb2.md
@@ -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: ``.
+
+```js
+assert.exists(document.querySelector('h1'));
+```
+
+Your `h1` element should have a closing tag. Closing tags have this syntax: ``.
+
+```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
+
+
+--fcc-editable-region--
+
+--fcc-editable-region--
+
+
+```
diff --git a/curriculum/challenges/english/28-basic-html/cat-photo-app/5dc1798ff86c76b9248c6eb3.md b/curriculum/challenges/english/28-basic-html/cat-photo-app/5dc1798ff86c76b9248c6eb3.md
new file mode 100644
index 00000000000..f6fff465a78
--- /dev/null
+++ b/curriculum/challenges/english/28-basic-html/cat-photo-app/5dc1798ff86c76b9248c6eb3.md
@@ -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: ``.
+
+```js
+assert.exists(document.querySelector('h1'));
+```
+
+Your `h1` element should have a closing tag. Closing tags have this syntax: ``.
+
+```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: ``.
+
+```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
+
+
+--fcc-editable-region--
+ CatPhotoApp
+
+--fcc-editable-region--
+
+
+```
diff --git a/curriculum/challenges/english/28-basic-html/cat-photo-app/5dc17d3bf86c76b9248c6eb4.md b/curriculum/challenges/english/28-basic-html/cat-photo-app/5dc17d3bf86c76b9248c6eb4.md
new file mode 100644
index 00000000000..eae0ea60c5a
--- /dev/null
+++ b/curriculum/challenges/english/28-basic-html/cat-photo-app/5dc17d3bf86c76b9248c6eb4.md
@@ -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: ``.
+
+```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
+
+
+ CatPhotoApp
+--fcc-editable-region--
+ Cat Photos
+
+--fcc-editable-region--
+
+
+```
diff --git a/curriculum/challenges/english/28-basic-html/cat-photo-app/5dc17dc8f86c76b9248c6eb5.md b/curriculum/challenges/english/28-basic-html/cat-photo-app/5dc17dc8f86c76b9248c6eb5.md
new file mode 100644
index 00000000000..224bba20d8d
--- /dev/null
+++ b/curriculum/challenges/english/28-basic-html/cat-photo-app/5dc17dc8f86c76b9248c6eb5.md
@@ -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 ``.
+
+Here is an example of a comment with the `TODO: Remove h1`:
+
+```html
+
+```
+
+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 end of a comment.
+
+```js
+assert.match(code, /-->/);
+```
+
+Your code should not have extra opening/closing comment characters. You have an extra `` displaying in the browser.
+
+```js
+const noSpaces = code.replace(/\s/g, '');
+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, ''),
+ /everyonelovescutecatsonline!<\/p>/i
+);
+```
+
+Your comment should contain the text `TODO: Add link to cat photos`.
+
+```js
+assert.match(code, //i);
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+ CatPhotoApp
+ Cat Photos
+--fcc-editable-region--
+
+ Everyone loves cute cats online!
+
+--fcc-editable-region--
+
+
+```
+
+# --solutions--
+
+```html
+
+
+ CatPhotoApp
+ Cat Photos
+
+ Everyone loves cute cats online!
+
+
+```
diff --git a/curriculum/challenges/english/28-basic-html/recipe-page/668f08ea07b99b1f4a91acab.md b/curriculum/challenges/english/28-basic-html/recipe-page/668f08ea07b99b1f4a91acab.md
new file mode 100644
index 00000000000..a84e101e1a7
--- /dev/null
+++ b/curriculum/challenges/english/28-basic-html/recipe-page/668f08ea07b99b1f4a91acab.md
@@ -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, //i);
+```
+
+You should have an `html` element with `lang` set to `en`.
+
+```js
+assert.match(code, /[\s\S]*<\/\s*html\s*>/gi);
+```
+
+You should have a `head` element within the `html` element.
+
+```js
+assert.match(code, /[\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
+
+
+
+
+
+ Chocolate chip cookies recipe
+
+
+
+ Chocolate Chip Cookies
+ 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.
+
+ Ingredients
+
+ - 1 cup all-purpose flour
+ - 1/2 teaspoon baking soda
+ - 1/4 cup unsalted butter, softened
+ - 1/4 cup granulated sugar
+ - 1/2 teaspoon vanilla extract
+ - 1/2 cup mini chocolate chips
+
+ Instructions
+
+ - Preheat your oven to 350°F (175°C) and line a baking sheet with parchment paper.
+ - In a bowl, whisk together the flour and baking soda.
+ - In another bowl, beat the butter, sugar, and vanilla extract until creamy.
+ - Gradually add the dry ingredients to the wet mixture, then fold in the mini chocolate chips.
+ - Drop small spoonfuls of dough onto the baking sheet.
+ - Bake for 8-10 minutes, then let cool before enjoying!
+
+
+
+
+```
diff --git a/curriculum/challenges/english/29-semantic-html/cat-blog-page/669aff9f5488f1bea056416d.md b/curriculum/challenges/english/29-semantic-html/cat-blog-page/669aff9f5488f1bea056416d.md
new file mode 100644
index 00000000000..2865934d5f8
--- /dev/null
+++ b/curriculum/challenges/english/29-semantic-html/cat-blog-page/669aff9f5488f1bea056416d.md
@@ -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 ``, 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
+
+
+
+
+```
+
+# --hints--
+
+You should have the ``.
+
+```js
+assert.match(code, //i);
+```
+
+You should have an opening `html` tag with the language set to english.
+
+```js
+assert.match(code, //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, /[.\n\s]*/im)
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+--fcc-editable-region--
+
+--fcc-editable-region--
+```
diff --git a/curriculum/challenges/english/29-semantic-html/cat-blog-page/669fc7e141e4703748c558bf.md b/curriculum/challenges/english/29-semantic-html/cat-blog-page/669fc7e141e4703748c558bf.md
new file mode 100644
index 00000000000..9213298dcd8
--- /dev/null
+++ b/curriculum/challenges/english/29-semantic-html/cat-blog-page/669fc7e141e4703748c558bf.md
@@ -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, //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, /[.\n\s]*<\/head>/im)
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+--fcc-editable-region--
+
+--fcc-editable-region--
+
+```
diff --git a/curriculum/challenges/english/29-semantic-html/cat-blog-page/669fc938d38e6e38ace9251e.md b/curriculum/challenges/english/29-semantic-html/cat-blog-page/669fc938d38e6e38ace9251e.md
new file mode 100644
index 00000000000..54f45919cbc
--- /dev/null
+++ b/curriculum/challenges/english/29-semantic-html/cat-blog-page/669fc938d38e6e38ace9251e.md
@@ -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 ``.
+
+```js
+assert.notMatch(code, /<\/meta>/i);
+```
+
+Your `meta` tag should have a `charset` attribute.
+
+```js
+assert.match(code, / meta');
+assert.strictEqual(meta?.parentElement?.tagName, 'HEAD');
+```
+
+You should have an opening `title` tag.
+
+```js
+assert.match(code, //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, /.*\s*.*<\/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
+
+
+ --fcc-editable-region--
+
+
+
+ --fcc-editable-region--
+
+```
diff --git a/curriculum/challenges/english/29-semantic-html/cat-blog-page/669fcb06c3034a39f5431a38.md b/curriculum/challenges/english/29-semantic-html/cat-blog-page/669fcb06c3034a39f5431a38.md
new file mode 100644
index 00000000000..e2a63b345c2
--- /dev/null
+++ b/curriculum/challenges/english/29-semantic-html/cat-blog-page/669fcb06c3034a39f5431a38.md
@@ -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 `` tag.
+
+```js
+assert.match(code, //i);
+```
+
+You should have a closing `` 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, //i);
+assert.match(code, /<\/head>/i);
+```
+
+Your `body` element should come after your `head` element.
+
+```js
+assert.match(code, /<\/head>[.\n\s]*/im)
+```
+
+# --seed--
+
+## --seed-contents--
+
+```html
+
+
+ --fcc-editable-region--
+
+ Mr. Whiskers' Blog
+
+
+
+ --fcc-editable-region--
+
+```
+
+
+# --solutions--
+
+```html
+
+
+
+ Mr. Whiskers' Blog
+
+
+
+
+
+```
diff --git a/curriculum/challenges/english/29-semantic-html/event-hub/66ebd4ae2812430bb883c787.md b/curriculum/challenges/english/29-semantic-html/event-hub/66ebd4ae2812430bb883c787.md
new file mode 100644
index 00000000000..daebc74e2ac
--- /dev/null
+++ b/curriculum/challenges/english/29-semantic-html/event-hub/66ebd4ae2812430bb883c787.md
@@ -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
+
+
+
+
+
+ Event Hub
+
+
+
+
+
+
+
+```
+
+# --solutions--
+
+```html
+
+
+
+
+
+ Event Hub
+
+
+
+
+
+ Upcoming Events
+
+ AI & Machine Learning Conference 2024
+ 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.
+ Date: August 10, 2024
+
+
+ Web Development Bootcamp
+ 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.
+ Date: September 5, 2024
+
+
+
+ Past Events
+
+ Cybersecurity Summit 2024
+ 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.
+ Date: June 15, 2024
+
+
+
+ Blockchain Expo 2024
+ A comprehensive event covering the future of blockchain technology. Topics included decentralized finance (DeFi), smart contracts, and the impact of blockchain on various industries.
+ Date: July 20, 2024
+
+
+
+
+
+
+```
+
diff --git a/curriculum/schema/challenge-schema.js b/curriculum/schema/challenge-schema.js
index 7dcd81689cd..e0b46c926f6 100644
--- a/curriculum/schema/challenge-schema.js
+++ b/curriculum/schema/challenge-schema.js
@@ -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',
diff --git a/curriculum/test/utils/mongo-ids.js b/curriculum/test/utils/mongo-ids.js
index a2bf8a6b930..a49a4a76c83 100644
--- a/curriculum/test/utils/mongo-ids.js
+++ b/curriculum/test/utils/mongo-ids.js
@@ -52,6 +52,18 @@ const duplicatedProjectIds = [
'5ef9b03c81a63668521804ee',
'62bb4009e3458a128ff57d5d',
+ // Recipe Page
+ '668f08ea07b99b1f4a91acab',
+
+ // Cat Blog
+ '669aff9f5488f1bea056416d',
+ '669fc7e141e4703748c558bf',
+ '669fc938d38e6e38ace9251e',
+ '669fcb06c3034a39f5431a38',
+
+ // Event hub
+ '66ebd4ae2812430bb883c787',
+
// Survey Form
'587d78af367417b2b2512b03',
diff --git a/curriculum/utils.test.ts b/curriculum/utils.test.ts
index 21555e193e6..6efba63983b 100644
--- a/curriculum/utils.test.ts
+++ b/curriculum/utils.test.ts
@@ -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');
diff --git a/shared/config/catalog.test.ts b/shared/config/catalog.test.ts
new file mode 100644
index 00000000000..42b46c521ba
--- /dev/null
+++ b/shared/config/catalog.test.ts
@@ -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()
+ );
+ });
+});
diff --git a/shared/config/catalog.ts b/shared/config/catalog.ts
new file mode 100644
index 00000000000..0caf294f18c
--- /dev/null
+++ b/shared/config/catalog.ts
@@ -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
+ }
+];
diff --git a/shared/config/certification-settings.ts b/shared/config/certification-settings.ts
index fd2a3ac85b6..ae22dc95b69 100644
--- a/shared/config/certification-settings.ts
+++ b/shared/config/certification-settings.ts
@@ -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
};
diff --git a/shared/config/constants.ts b/shared/config/constants.ts
index 8087e96e6c8..9916d6b028d 100644
--- a/shared/config/constants.ts
+++ b/shared/config/constants.ts
@@ -74,6 +74,7 @@ export const blocklistedUsernames = [
'backend-challenge-completed',
'blocked',
'bonfire',
+ 'catalog',
'cats.json',
'challenge-completed',
'challenge',
diff --git a/shared/config/curriculum.test.ts b/shared/config/curriculum.test.ts
index 8dd24f37803..1dc3bdc4a49 100644
--- a/shared/config/curriculum.test.ts
+++ b/shared/config/curriculum.test.ts
@@ -33,6 +33,7 @@ describe('generateSuperBlockList', () => {
});
const tempSuperBlockMap = { ...superBlockStages };
tempSuperBlockMap[SuperBlockStage.Upcoming] = [];
+ tempSuperBlockMap[SuperBlockStage.Catalog] = [];
expect(result).toHaveLength(Object.values(tempSuperBlockMap).flat().length);
});
});
diff --git a/shared/config/curriculum.ts b/shared/config/curriculum.ts
index bd72fa709e8..c9d40ce0390 100644
--- a/shared/config/curriculum.ts
+++ b/shared/config/curriculum.ts
@@ -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
]
};
diff --git a/tools/challenge-editor/api/configs/super-block-list.ts b/tools/challenge-editor/api/configs/super-block-list.ts
index ada294e1dcc..342ca19959e 100644
--- a/tools/challenge-editor/api/configs/super-block-list.ts
+++ b/tools/challenge-editor/api/configs/super-block-list.ts
@@ -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'
}
];
diff --git a/tools/scripts/build/build-external-curricula-data-v1.test.ts b/tools/scripts/build/build-external-curricula-data-v1.test.ts
index 25c8c067200..4216302701d 100644
--- a/tools/scripts/build/build-external-curricula-data-v1.test.ts
+++ b/tools/scripts/build/build-external-curricula-data-v1.test.ts
@@ -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);
diff --git a/tools/scripts/build/build-external-curricula-data-v2.test.ts b/tools/scripts/build/build-external-curricula-data-v2.test.ts
index 3a7dafc60cc..477f178d5c2 100644
--- a/tools/scripts/build/build-external-curricula-data-v2.test.ts
+++ b/tools/scripts/build/build-external-curricula-data-v2.test.ts
@@ -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();