From 3af161450fe35692dd7d3e1d67ca426756c2a7cd Mon Sep 17 00:00:00 2001 From: Tom <20648924+moT01@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:08:36 -0500 Subject: [PATCH] feat(curriculum, client): add catalog (#60951) --- client/i18n/locales.test.ts | 12 +- client/i18n/locales/english/intro.json | 43 +++ client/i18n/locales/english/translations.json | 12 + client/src/assets/superblock-icon.tsx | 2 + client/src/components/Map/index.tsx | 3 +- client/src/pages/catalog.css | 22 ++ client/src/pages/catalog.tsx | 61 ++++ .../learn/basic-html/cat-photo-app/index.md | 11 + client/src/pages/learn/basic-html/index.md | 9 + .../learn/basic-html/recipe-page/index.md | 9 + .../semantic-html/cat-blog-page/index.md | 9 + .../learn/semantic-html/event-hub/index.md | 9 + client/src/pages/learn/semantic-html/index.md | 9 + .../challenges/_meta/cat-blog-page/meta.json | 30 ++ .../challenges/_meta/cat-photo-app/meta.json | 30 ++ .../challenges/_meta/event-hub/meta.json | 12 + .../challenges/_meta/recipe-page/meta.json | 17 + .../cat-photo-app/5dc174fcf86c76b9248c6eb2.md | 54 +++ .../cat-photo-app/5dc1798ff86c76b9248c6eb3.md | 80 +++++ .../cat-photo-app/5dc17d3bf86c76b9248c6eb4.md | 60 ++++ .../cat-photo-app/5dc17dc8f86c76b9248c6eb5.md | 88 +++++ .../recipe-page/668f08ea07b99b1f4a91acab.md | 224 ++++++++++++ .../cat-blog-page/669aff9f5488f1bea056416d.md | 58 +++ .../cat-blog-page/669fc7e141e4703748c558bf.md | 43 +++ .../cat-blog-page/669fc938d38e6e38ace9251e.md | 87 +++++ .../cat-blog-page/669fcb06c3034a39f5431a38.md | 69 ++++ .../event-hub/66ebd4ae2812430bb883c787.md | 335 ++++++++++++++++++ curriculum/schema/challenge-schema.js | 7 +- curriculum/test/utils/mongo-ids.js | 12 + curriculum/utils.test.ts | 4 +- shared/config/catalog.test.ts | 10 + shared/config/catalog.ts | 26 ++ shared/config/certification-settings.ts | 2 + shared/config/constants.ts | 1 + shared/config/curriculum.test.ts | 1 + shared/config/curriculum.ts | 39 +- .../api/configs/super-block-list.ts | 8 + .../build-external-curricula-data-v1.test.ts | 4 +- .../build-external-curricula-data-v2.test.ts | 4 +- 39 files changed, 1504 insertions(+), 12 deletions(-) create mode 100644 client/src/pages/catalog.css create mode 100644 client/src/pages/catalog.tsx create mode 100644 client/src/pages/learn/basic-html/cat-photo-app/index.md create mode 100644 client/src/pages/learn/basic-html/index.md create mode 100644 client/src/pages/learn/basic-html/recipe-page/index.md create mode 100644 client/src/pages/learn/semantic-html/cat-blog-page/index.md create mode 100644 client/src/pages/learn/semantic-html/event-hub/index.md create mode 100644 client/src/pages/learn/semantic-html/index.md create mode 100644 curriculum/challenges/_meta/cat-blog-page/meta.json create mode 100644 curriculum/challenges/_meta/cat-photo-app/meta.json create mode 100644 curriculum/challenges/_meta/event-hub/meta.json create mode 100644 curriculum/challenges/_meta/recipe-page/meta.json create mode 100644 curriculum/challenges/english/28-basic-html/cat-photo-app/5dc174fcf86c76b9248c6eb2.md create mode 100644 curriculum/challenges/english/28-basic-html/cat-photo-app/5dc1798ff86c76b9248c6eb3.md create mode 100644 curriculum/challenges/english/28-basic-html/cat-photo-app/5dc17d3bf86c76b9248c6eb4.md create mode 100644 curriculum/challenges/english/28-basic-html/cat-photo-app/5dc17dc8f86c76b9248c6eb5.md create mode 100644 curriculum/challenges/english/28-basic-html/recipe-page/668f08ea07b99b1f4a91acab.md create mode 100644 curriculum/challenges/english/29-semantic-html/cat-blog-page/669aff9f5488f1bea056416d.md create mode 100644 curriculum/challenges/english/29-semantic-html/cat-blog-page/669fc7e141e4703748c558bf.md create mode 100644 curriculum/challenges/english/29-semantic-html/cat-blog-page/669fc938d38e6e38ace9251e.md create mode 100644 curriculum/challenges/english/29-semantic-html/cat-blog-page/669fcb06c3034a39f5431a38.md create mode 100644 curriculum/challenges/english/29-semantic-html/event-hub/66ebd4ae2812430bb883c787.md create mode 100644 shared/config/catalog.test.ts create mode 100644 shared/config/catalog.ts 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 for baking: three eggs, a bowl of flour, a glass of milk, and a whisk arranged on a wooden table. +

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

+
    +
  1. Preheat your oven to 350°F (175°C) and line a baking sheet with parchment paper.
  2. +
  3. In a bowl, whisk together the flour and baking soda.
  4. +
  5. In another bowl, beat the butter, sugar, and vanilla extract until creamy.
  6. +
  7. Gradually add the dry ingredients to the wet mixture, then fold in the mini chocolate chips.
  8. +
  9. Drop small spoonfuls of dough onto the baking sheet.
  10. +
  11. Bake for 8-10 minutes, then let cool before enjoying!
  12. +
+ + + +``` 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, /<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> +``` 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 `<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 + + + + --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 + + +
+

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

+ Image from Cybersecurity Summit 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

+ Image from Blockchain Expo 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();