mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(curriculum, client): add catalog (#60951)
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { setup } from 'jest-json-schema-extended';
|
import { setup } from 'jest-json-schema-extended';
|
||||||
import { availableLangs, LangNames, LangCodes } from '../../shared/config/i18n';
|
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';
|
import intro from './locales/english/intro.json';
|
||||||
|
|
||||||
setup();
|
setup();
|
||||||
@@ -9,6 +12,7 @@ setup();
|
|||||||
interface Intro {
|
interface Intro {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
title: string;
|
title: string;
|
||||||
|
summary?: string[];
|
||||||
intro: string[];
|
intro: string[];
|
||||||
blocks: {
|
blocks: {
|
||||||
[block: string]: {
|
[block: string]: {
|
||||||
@@ -74,6 +78,12 @@ describe('Intro file structure tests:', () => {
|
|||||||
const superblocks = Object.values(SuperBlocks);
|
const superblocks = Object.values(SuperBlocks);
|
||||||
for (const superBlock of superblocks) {
|
for (const superBlock of superblocks) {
|
||||||
expect(typeof typedIntro[superBlock].title).toBe('string');
|
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].intro).toBeInstanceOf(Array);
|
||||||
expect(typedIntro[superBlock].blocks).toBeInstanceOf(Object);
|
expect(typedIntro[superBlock].blocks).toBeInstanceOf(Object);
|
||||||
const blocks = Object.keys(typedIntro[superBlock].blocks);
|
const blocks = Object.keys(typedIntro[superBlock].blocks);
|
||||||
|
|||||||
@@ -4553,6 +4553,49 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"basic-html": {
|
||||||
|
"title": "Basic HTML",
|
||||||
|
"summary": [
|
||||||
|
"Learn how to build simple webpages using HTML tags to add text, images, and links."
|
||||||
|
],
|
||||||
|
"intro": ["Larger intro for the superblock page."],
|
||||||
|
"blocks": {
|
||||||
|
"cat-photo-app": {
|
||||||
|
"title": "Build a Cat Photo App",
|
||||||
|
"intro": [
|
||||||
|
"HTML tags give a webpage its structure. You can use HTML tags to add photos, buttons, and other elements to your webpage.",
|
||||||
|
"In this course, you'll learn the most common HTML tags by building your own cat photo app."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"recipe-page": {
|
||||||
|
"title": "Build a Recipe Page",
|
||||||
|
"intro": [
|
||||||
|
"In this lab, you'll review HTML basics by creating a web page of your favorite recipe. You'll create an HTML boilerplate and work with headings, lists, images, and more."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"semantic-html": {
|
||||||
|
"title": "Semantic HTML",
|
||||||
|
"summary": [
|
||||||
|
"Discover how to write cleaner, more meaningful HTML using semantic tags that improve structure, accessibility, and SEO."
|
||||||
|
],
|
||||||
|
"intro": ["Larger intro for the superblock page."],
|
||||||
|
"blocks": {
|
||||||
|
"cat-blog-page": {
|
||||||
|
"title": "Build a Cat Blog Page",
|
||||||
|
"intro": [
|
||||||
|
"In this workshop, you will build an HTML only blog page using semantic elements including the <code>main</code>, <code>nav</code>, <code>article</code> and <code>footer</code> elements."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event-hub": {
|
||||||
|
"title": "Build an Event Hub",
|
||||||
|
"intro": [
|
||||||
|
"In this lab, you'll build an event hub and review semantic elements like <code>header</code>, <code>nav</code>, <code>article</code>, and more."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dev-playground": {
|
"dev-playground": {
|
||||||
"title": "Dev Playground",
|
"title": "Dev Playground",
|
||||||
"intro": ["Playground for creating and testing challenges"],
|
"intro": ["Playground for creating and testing challenges"],
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
"click-start-course": "Start the course",
|
"click-start-course": "Start the course",
|
||||||
"click-start-project": "Start the project",
|
"click-start-project": "Start the project",
|
||||||
"click-start-exam": "Start the exam",
|
"click-start-exam": "Start the exam",
|
||||||
|
"go-to-course": "Go to course",
|
||||||
"change-language": "Change Language",
|
"change-language": "Change Language",
|
||||||
"resume-project": "Resume project",
|
"resume-project": "Resume project",
|
||||||
"start-project": "Start project",
|
"start-project": "Start project",
|
||||||
@@ -175,6 +176,7 @@
|
|||||||
"legacy-curriculum-heading": "Our archived coursework:",
|
"legacy-curriculum-heading": "Our archived coursework:",
|
||||||
"next-heading": "Try our beta curriculum:",
|
"next-heading": "Try our beta curriculum:",
|
||||||
"upcoming-heading": "Upcoming curriculum:",
|
"upcoming-heading": "Upcoming curriculum:",
|
||||||
|
"catalog-heading": "Explore our Catalog:",
|
||||||
"faq": "Frequently asked questions:",
|
"faq": "Frequently asked questions:",
|
||||||
"faqs": [
|
"faqs": [
|
||||||
{
|
{
|
||||||
@@ -1245,5 +1247,15 @@
|
|||||||
"exit": "Exit the survey",
|
"exit": "Exit the survey",
|
||||||
"two-questions": "Congratulations on getting this far. Before you can start the exam, please answer these two short survey questions."
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ const iconMap = {
|
|||||||
[SuperBlocks.A2Chinese]: A2EnglishIcon,
|
[SuperBlocks.A2Chinese]: A2EnglishIcon,
|
||||||
[SuperBlocks.RosettaCode]: RosettaCodeIcon,
|
[SuperBlocks.RosettaCode]: RosettaCodeIcon,
|
||||||
[SuperBlocks.PythonForEverybody]: PythonIcon,
|
[SuperBlocks.PythonForEverybody]: PythonIcon,
|
||||||
|
[SuperBlocks.BasicHtml]: Code,
|
||||||
|
[SuperBlocks.SemanticHtml]: Code,
|
||||||
[SuperBlocks.DevPlayground]: Code
|
[SuperBlocks.DevPlayground]: Code
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ const superBlockHeadings: { [key in SuperBlockStage]: string } = {
|
|||||||
[SuperBlockStage.Extra]: 'landing.interview-prep-heading',
|
[SuperBlockStage.Extra]: 'landing.interview-prep-heading',
|
||||||
[SuperBlockStage.Legacy]: 'landing.legacy-curriculum-heading',
|
[SuperBlockStage.Legacy]: 'landing.legacy-curriculum-heading',
|
||||||
[SuperBlockStage.Next]: 'landing.next-heading',
|
[SuperBlockStage.Next]: 'landing.next-heading',
|
||||||
[SuperBlockStage.Upcoming]: 'landing.upcoming-heading'
|
[SuperBlockStage.Upcoming]: 'landing.upcoming-heading',
|
||||||
|
[SuperBlockStage.Catalog]: 'landing.catalog-heading'
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Col, Spacer } from '@freecodecamp/ui';
|
||||||
|
import { ButtonLink } from '../components/helpers';
|
||||||
|
import { catalog } from '../../../shared/config/catalog';
|
||||||
|
import { showUpcomingChanges } from '../../config/env.json';
|
||||||
|
import FourOhFour from '../components/FourOhFour';
|
||||||
|
|
||||||
|
import './catalog.css';
|
||||||
|
|
||||||
|
const CatalogPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return showUpcomingChanges ? (
|
||||||
|
<main>
|
||||||
|
<Spacer size='l' />
|
||||||
|
<h1 className='text-center'>{t('curriculum.catalog.title')}</h1>
|
||||||
|
<Spacer size='l' />
|
||||||
|
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
|
<section className='catalog-wrap'>
|
||||||
|
{catalog.map(course => {
|
||||||
|
const { superBlock, level, hours } = course;
|
||||||
|
|
||||||
|
const { title, summary } = t(`intro:${superBlock}`, {
|
||||||
|
returnObjects: true
|
||||||
|
}) as {
|
||||||
|
title: string;
|
||||||
|
summary: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='catalog-item' key={superBlock}>
|
||||||
|
<div className='catalog-item-top'>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<hr />
|
||||||
|
{summary.map(text => (
|
||||||
|
<p key='text'>{text}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className='catalog-item-bottom'>
|
||||||
|
<div>
|
||||||
|
{t(`curriculum.catalog.levels.${level}`)} • {hours}{' '}
|
||||||
|
hours
|
||||||
|
</div>
|
||||||
|
<ButtonLink href={`/learn/${superBlock}`}>
|
||||||
|
{t('buttons.go-to-course')}
|
||||||
|
</ButtonLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</Col>
|
||||||
|
<Spacer size='l' />
|
||||||
|
</main>
|
||||||
|
) : (
|
||||||
|
<FourOhFour />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CatalogPage;
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: Basic HTML
|
||||||
|
superBlock: basic-html
|
||||||
|
certification: basic-html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction to Basic HTML
|
||||||
|
|
||||||
|
Intoduction to Basic HTML.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: Introduction to the Build a Cat Blog Page
|
||||||
|
block: cat-blog-page
|
||||||
|
superBlock: semantic-html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction to the Build a Cat Blog Page
|
||||||
|
|
||||||
|
In this workshop, you will build an HTML only blog page using semantic elements including the <code>main</code>, <code>nav</code>, <code>article</code> and <code>footer</code> elements.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: Semantic HTML
|
||||||
|
superBlock: semantic-html
|
||||||
|
certification: semantic-html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction to Semantic HTML
|
||||||
|
|
||||||
|
Intoduction to Semantic HTML.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
id: 5dc174fcf86c76b9248c6eb2
|
||||||
|
title: Step 1
|
||||||
|
challengeType: 0
|
||||||
|
dashedName: step-1
|
||||||
|
demoType: onLoad
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
In this workshop, you will continue working with basic HTML elements like headings, paragraphs, and lists by building a cat photo app.
|
||||||
|
|
||||||
|
Begin the workshop by adding an `h1` element with the text of `CatPhotoApp`.
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
The text `CatPhotoApp` should be present in the code. You may want to check your spelling.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /catphotoapp/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `h1` element should have an opening tag. Opening tags have this syntax: `<elementName>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.exists(document.querySelector('h1'));
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `h1` element should have a closing tag. Closing tags have this syntax: `</elementName>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\/h1\>/);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `h1` element's text should be `CatPhotoApp`. You have either omitted the text, have a typo, or it is not between the `h1` element's opening and closing tags.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.equal(document.querySelector('h1')?.innerText.toLowerCase(), 'catphotoapp');
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```html
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
--fcc-editable-region--
|
||||||
|
|
||||||
|
--fcc-editable-region--
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
id: 5dc1798ff86c76b9248c6eb3
|
||||||
|
title: Step 2
|
||||||
|
challengeType: 0
|
||||||
|
dashedName: step-2
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
Below the `h1` element, add an `h2` element with this text:
|
||||||
|
|
||||||
|
`Cat Photos`
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
Your `h1` element should have an opening tag. Opening tags have this syntax: `<elementName>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.exists(document.querySelector('h1'));
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `h1` element should have a closing tag. Closing tags have this syntax: `</elementName>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\/h1\>/);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should only have one `h1` element. Remove the extra.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.lengthOf(document.querySelectorAll('h1'), 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `h1` element's text should be 'CatPhotoApp'. You have either omitted the text or have a typo.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.equal(document.querySelector('h1')?.innerText.toLowerCase(), 'catphotoapp');
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `h2` element should have an opening tag. Opening tags have this syntax: `<elementName>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.exists(document.querySelector('h2'));
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `h2` element should have a closing tag. Closing tags have a `/` just after the `<` character.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\/h2\>/);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `h2` element's text should be `Cat Photos`. Only place the text `Cat Photos` between the opening and closing `h2` tags.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.equal(document.querySelector('h2')?.innerText.toLowerCase(), 'cat photos');
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `h2` element should be below the `h1` element. The `h1` element has greater importance and must be above the `h2` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const collection = [...document.querySelectorAll('h1,h2')].map(
|
||||||
|
(node) => node.nodeName
|
||||||
|
);
|
||||||
|
assert.isBelow(collection.indexOf('H1'), collection.indexOf('H2'));
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```html
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
--fcc-editable-region--
|
||||||
|
<h1>CatPhotoApp</h1>
|
||||||
|
|
||||||
|
--fcc-editable-region--
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
id: 5dc17d3bf86c76b9248c6eb4
|
||||||
|
title: Step 3
|
||||||
|
challengeType: 0
|
||||||
|
dashedName: step-3
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
Create a `p` element below your `h2` element and give it the following text:
|
||||||
|
|
||||||
|
`Everyone loves cute cats online!`
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
Your `p` element should have an opening tag. Opening tags have the following syntax: `<elementName>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.exists(document.querySelector('p'));
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `p` element should have a closing tag. Closing tags have a `/` just after the `<` character.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\/p\>/);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `p` element's text should be `Everyone loves cute cats online!` You have either omitted the text or have a typo.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const extraSpacesRemoved = document
|
||||||
|
.querySelector('p')
|
||||||
|
?.innerText.replace(/\s+/g, ' ');
|
||||||
|
assert.match(extraSpacesRemoved, /everyone loves cute cats online!$/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `p` element should be below the `h2` element. You have them in the wrong order.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const collection = [...document.querySelectorAll('h2,p')].map(
|
||||||
|
(node) => node.nodeName
|
||||||
|
);
|
||||||
|
assert.isBelow(collection.indexOf('H2'), collection.indexOf('P'));
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```html
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>CatPhotoApp</h1>
|
||||||
|
--fcc-editable-region--
|
||||||
|
<h2>Cat Photos</h2>
|
||||||
|
|
||||||
|
--fcc-editable-region--
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
id: 5dc17dc8f86c76b9248c6eb5
|
||||||
|
title: Step 4
|
||||||
|
challengeType: 0
|
||||||
|
dashedName: step-4
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
Commenting allows you to leave messages without affecting the browser display. It also allows you to make code inactive. A comment in HTML starts with `<!--`, contains any number of lines of text, and ends with `-->`.
|
||||||
|
|
||||||
|
Here is an example of a comment with the `TODO: Remove h1`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- TODO: Remove h1 -->
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a comment above the `p` element with this text:
|
||||||
|
|
||||||
|
`TODO: Add link to cat photos`
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
Your comment should start with `<!--`. You are missing one or more of the characters that define the start of a comment.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<!--/);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your comment should end with `-->`. You are missing one or more of the characters that define the end of a comment.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /-->/);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your code should not have extra opening/closing comment characters. You have an extra `<!--` or `-->` displaying in the browser.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const noSpaces = code.replace(/\s/g, '');
|
||||||
|
assert.isBelow(noSpaces.match(/<!--/g)?.length, 2)
|
||||||
|
assert.isBelow(noSpaces.match(/-->/g)?.length, 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your comment should be above the `p` element. You have them in the wrong order.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(
|
||||||
|
code.replace(/\s/g, ''),
|
||||||
|
/<!--(.*?)--><p>everyonelovescutecatsonline!<\/p>/i
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your comment should contain the text `TODO: Add link to cat photos`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<!--\s*todo:\s+add\s+link\s+to\s+cat\s+photos\s*-->/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```html
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>CatPhotoApp</h1>
|
||||||
|
<h2>Cat Photos</h2>
|
||||||
|
--fcc-editable-region--
|
||||||
|
|
||||||
|
<p>Everyone loves cute cats online!</p>
|
||||||
|
|
||||||
|
--fcc-editable-region--
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
# --solutions--
|
||||||
|
|
||||||
|
```html
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>CatPhotoApp</h1>
|
||||||
|
<h2>Cat Photos</h2>
|
||||||
|
<!-- TODO: Add link to cat photos -->
|
||||||
|
<p>Everyone loves cute cats online!</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
---
|
||||||
|
id: 668f08ea07b99b1f4a91acab
|
||||||
|
title: Build a Recipe Page
|
||||||
|
challengeType: 25
|
||||||
|
dashedName: build-a-recipe-page
|
||||||
|
demoType: onClick
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
Fulfill the user stories below and get all the tests to pass to complete the lab.
|
||||||
|
|
||||||
|
**User Stories:**
|
||||||
|
|
||||||
|
1. You should have a `!DOCTYPE html` declaration.
|
||||||
|
1. You should have an `html` element with `lang` set to `en`.
|
||||||
|
1. You should have a `head` element containing a `title` element with the name of your recipe, and a `meta` element with a `charset` attribute set to `UTF-8`.
|
||||||
|
1. You should have a `body` element.
|
||||||
|
1. You should have an `h1` element with the name of your recipe.
|
||||||
|
1. You should have a `p` element that introduces the recipe below the `h1`.
|
||||||
|
1. You should have one `h2` element with the text `Ingredients` for the ingredients section.
|
||||||
|
1. You should have an unordered list (`ul` element) with at least four list items (`li` elements) that lists your ingredients below the first `h2` element.
|
||||||
|
1. You should have a second `h2` element with the text `Instructions` for the instructions section.
|
||||||
|
1. You should have an ordered list (`ol` element) with at least four list items that lists the recipe steps in order, below the second `h2`.
|
||||||
|
1. You should have one `img` element with a `src` attribute set to a valid image, you can use `https://cdn.freecodecamp.org/curriculum/labs/recipe.jpg` if you would like, and an `alt` attribute describing the image.
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
Your recipe page should have a `!DOCTYPE html` declaration.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<!DOCTYPE html>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have an `html` element with `lang` set to `en`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<html\s+lang\s*=\s*('|")en\1\s*>[\s\S]*<\/\s*html\s*>/gi);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have a `head` element within the `html` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<html[\s\S]*>[\s\S]*<\s*head\s*>[\s\S]*<\/\s*head\s*>[\s\S]*<\/\s*html\s*>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have `title` element within your `head` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\s*head\s*>[\s\S]*<\s*title\s*>[\s\S]*<\/\s*title\s*>[\s\S]*<\/\s*head\s*>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `title` element should have your recipe title.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.isAbove(document.querySelector('title')?.innerText.trim().length, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have a `meta` element within your `head` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\s*head\s*>[\s\S]*<\s*meta[\s\S]*>[\s\S]*<\/\s*head\s*>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `meta` element should have its `charset` attribute set to `UTF-8`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\s*meta[\s\S]+?charset\s*=\s*('|")UTF-8\1/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have a `body` element within your `html` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\s*html[\s\S]*>[\s\S]*<\s*head\s*>[\s\S]*<\/\s*head\s*>[\s\S]*<\s*body\s*>[\s\S]*<\/\s*body\s*>[\s\S]*<\/\s*html\s*>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have an `h1` element with the name of your recipe.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.isAbove(document.querySelector('h1')?.innerText.length, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should only have one `h1` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.lengthOf(document.querySelectorAll('h1'), 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have a `p` element below your `h1` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.strictEqual(document.querySelector('h1')?.nextElementSibling, document.querySelector('p'));
|
||||||
|
```
|
||||||
|
|
||||||
|
Your first `p` element should describe your recipe.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.isNotEmpty(document.querySelector('p')?.textContent?.trim());
|
||||||
|
```
|
||||||
|
|
||||||
|
Your first `h2` element should have the text `Ingredients`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.equal(document.querySelectorAll('h2')[0]?.innerText, 'Ingredients');
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have an unordered list element below your first `h2` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.strictEqual(document.querySelector('ul')?.previousElementSibling.tagName, 'H2');
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have at least four list item elements in your unordered list with the ingredients.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const els = document.querySelectorAll('ul > li');
|
||||||
|
assert.isAbove(els.length, 3);
|
||||||
|
els.forEach(el => assert.isAbove(el.innerText.trim().length, 0))
|
||||||
|
```
|
||||||
|
|
||||||
|
Your second `h2` element should have the text `Instructions`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.equal(document.querySelectorAll('h2')[1]?.innerText, 'Instructions');
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have an ordered list element below your second `h2` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.strictEqual(document.querySelectorAll('h2')?.[1]?.nextElementSibling?.tagName, "OL");
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have at least four list item elements in your ordered list with the instructions.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const els = document.querySelectorAll('ol > li');
|
||||||
|
assert.isAbove(els.length, 3);
|
||||||
|
els.forEach(el => assert.isAbove(el.innerText.trim().length, 0))
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have at least one `img` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.exists(document.querySelector('img'));
|
||||||
|
```
|
||||||
|
|
||||||
|
All your `img` elements should have a valid `src` attribute and value.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const img = document.querySelector('img');
|
||||||
|
const rawSrc = img?.getAttribute('src');
|
||||||
|
const resolvedSrc = img?.src;
|
||||||
|
const re = new RegExp(window.location.href, "ig");
|
||||||
|
|
||||||
|
assert.isAbove(rawSrc?.trim().length, 0, "The 'src' attribute must be explicitly set.");
|
||||||
|
assert.notMatch(resolvedSrc, re, "The 'src' should not start with the current page URL");
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
console.log('Image loaded successfully.');
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = (error) => {
|
||||||
|
console.error('Image failed to load:', error);
|
||||||
|
assert.fail("Your image's URL should be valid."); // Make the test instafail
|
||||||
|
};
|
||||||
|
|
||||||
|
if (img.complete) {
|
||||||
|
img.onload && img.onload();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
All your `img` elements should have an `alt` attribute to describe the image.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.isAbove(document.querySelector('img')?.alt.length, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```html
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
# --solutions--
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Chocolate chip cookies recipe</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Chocolate Chip Cookies</h1>
|
||||||
|
<p>Welcome to the ultimate guide for making mini chocolate chip cookies! These bite-sized treats are perfect for
|
||||||
|
satisfying your sweet tooth without overindulging. Follow this simple recipe to create delicious,
|
||||||
|
crispy-on-the-outside, chewy-on-the-inside mini chocolate chip cookies that everyone will love.</p>
|
||||||
|
<img src="https://cdn.freecodecamp.org/curriculum/labs/recipe.jpg" alt="Ingredients for baking: three eggs, a bowl of flour, a glass of milk, and a whisk arranged on a wooden table.">
|
||||||
|
<h2>Ingredients</h2>
|
||||||
|
<ul>
|
||||||
|
<li>1 cup all-purpose flour</li>
|
||||||
|
<li>1/2 teaspoon baking soda</li>
|
||||||
|
<li>1/4 cup unsalted butter, softened</li>
|
||||||
|
<li>1/4 cup granulated sugar</li>
|
||||||
|
<li>1/2 teaspoon vanilla extract</li>
|
||||||
|
<li>1/2 cup mini chocolate chips</li>
|
||||||
|
</ul>
|
||||||
|
<h2>Instructions</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Preheat your oven to 350°F (175°C) and line a baking sheet with parchment paper.</li>
|
||||||
|
<li>In a bowl, whisk together the flour and baking soda.</li>
|
||||||
|
<li>In another bowl, beat the butter, sugar, and vanilla extract until creamy.</li>
|
||||||
|
<li>Gradually add the dry ingredients to the wet mixture, then fold in the mini chocolate chips.</li>
|
||||||
|
<li>Drop small spoonfuls of dough onto the baking sheet.</li>
|
||||||
|
<li>Bake for 8-10 minutes, then let cool before enjoying!</li>
|
||||||
|
</ol>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
```
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
id: 669aff9f5488f1bea056416d
|
||||||
|
title: Step 1
|
||||||
|
challengeType: 0
|
||||||
|
dashedName: step-1
|
||||||
|
demoType: onLoad
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
In this workshop, you will practice working with semantic HTML by building a blog page dedicated to Mr. Whiskers the cat.
|
||||||
|
|
||||||
|
To begin the project, add the `<!DOCTYPE html>`, and an `html` element with a `lang` attribute of `en`.
|
||||||
|
|
||||||
|
Remember that you learned how to build a basic HTML boilerplate like this in the previous module.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<!--all other elements go here-->
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
You should have the `<!DOCTYPE html>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<!DOCTYPE\s+html>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have an opening `html` tag with the language set to english.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<html\s+lang\s*=\s*('|")en\1\s*>/gi);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have a closing `html` tag.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\/html>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `DOCTYPE` should come before the `html` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<!DOCTYPE\s+html>[.\n\s]*<html\s+lang\s*=\s*('|")en\1\s*>/im)
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```html
|
||||||
|
--fcc-editable-region--
|
||||||
|
|
||||||
|
--fcc-editable-region--
|
||||||
|
```
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
id: 669fc7e141e4703748c558bf
|
||||||
|
title: Step 2
|
||||||
|
challengeType: 0
|
||||||
|
dashedName: step-2
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
Inside the `html` element, add a `head` element.
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
You should have an opening `head` tag.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<head>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have a closing `head` tag.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\/head>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your opening `head` tag should come before the closing `head` tag.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<head>[.\n\s]*<\/head>/im)
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
--fcc-editable-region--
|
||||||
|
|
||||||
|
--fcc-editable-region--
|
||||||
|
</html>
|
||||||
|
```
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
id: 669fc938d38e6e38ace9251e
|
||||||
|
title: Step 3
|
||||||
|
challengeType: 0
|
||||||
|
dashedName: step-3
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
Inside your `head` element, nest a `meta` element with the `charset` attribute set to the value `"UTF-8"`.
|
||||||
|
|
||||||
|
Below that `meta` element, add a `title` element.
|
||||||
|
|
||||||
|
The `title` element's text should be `Mr. Whiskers' Blog`.
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
You should have a `meta` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.isNotNull(document.querySelector("meta"));
|
||||||
|
```
|
||||||
|
|
||||||
|
The `meta` element is a void element, it should not have an end tag `</meta>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.notMatch(code, /<\/meta>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `meta` tag should have a `charset` attribute.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<meta\s+charset\s*/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `charset` attribute should have a value of `"UTF-8"`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /charset\s*=\s*('|")UTF-8\1/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `meta` element should be nested inside your `head` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const meta = document.querySelector('head > meta');
|
||||||
|
assert.strictEqual(meta?.parentElement?.tagName, 'HEAD');
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have an opening `title` tag.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<title>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have a closing `title` tag.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\/title>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `title` element should be nested in your `head` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<head>.*\s*<title>.*<\/title>.*\s*<\/head>/si);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `title` element should have the text `Mr. Whiskers' Blog`. You may need to check your spelling.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const titleText = document.querySelector('title')?.innerText
|
||||||
|
assert.strictEqual(titleText, "Mr. Whiskers' Blog");
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
--fcc-editable-region--
|
||||||
|
<head>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
--fcc-editable-region--
|
||||||
|
</html>
|
||||||
|
```
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
id: 669fcb06c3034a39f5431a38
|
||||||
|
title: Step 4
|
||||||
|
challengeType: 0
|
||||||
|
dashedName: step-4
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
To prepare creating some actual content, add a `body` element below the `head` element.
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
You should have an opening `<body>` tag.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<body>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have a closing `</body>` tag.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\/body>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
You should not change your `head` element. Make sure you did not delete your closing tag.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<head>/i);
|
||||||
|
assert.match(code, /<\/head>/i);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `body` element should come after your `head` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.match(code, /<\/head>[.\n\s]*<body>/im)
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
--fcc-editable-region--
|
||||||
|
<head>
|
||||||
|
<title>Mr. Whiskers' Blog</title>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
--fcc-editable-region--
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# --solutions--
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Mr. Whiskers' Blog</title>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
---
|
||||||
|
id: 66ebd4ae2812430bb883c787
|
||||||
|
title: Build an Event Hub
|
||||||
|
challengeType: 25
|
||||||
|
dashedName: lab-event-hub
|
||||||
|
demoType: onClick
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
In this lab you will utilize the semantic HTML elements to create the structure of a web page. You'll add content and images to make it look like a real event hub.
|
||||||
|
|
||||||
|
Fulfill the user stories below and get all the tests to pass to complete the lab.
|
||||||
|
|
||||||
|
**User Stories:**
|
||||||
|
|
||||||
|
1. You should have a `header` element.
|
||||||
|
1. Inside the `header` element, you should have an `h1` element that contains the text `Event Hub`, and a `nav` element.
|
||||||
|
1. Inside the `nav` element, you should have an unordered list of two items containing links to different sections of the page. The first item should have the text `Upcoming Events`, and the second item should have the text `Past Events`.
|
||||||
|
1. Each link should be represented by an `a` element with an `href` attribute that links to the corresponding section of the page, `#upcoming-events` and `#past-events` respectively.
|
||||||
|
1. You should have a `main` element that contains the different sections of the page.
|
||||||
|
1. Inside the `main` element, you should have two `section` elements.
|
||||||
|
1. The first `section` element should have an `id` attribute with the value `upcoming-events`
|
||||||
|
1. Inside the `#upcoming-events` section, you should have:
|
||||||
|
|
||||||
|
- An `h2` element with the text `Upcoming Events`.
|
||||||
|
- Two `article` elements. Each article should represent an event, and it should have :
|
||||||
|
- An `h3` element for the event title.
|
||||||
|
- A `p` element for the event description. You can add a date at the bottom if you like.
|
||||||
|
|
||||||
|
1. The second `section` element should have an `id` attribute with the value `past-events`.
|
||||||
|
1. Inside the `#past-events` section, you should have:
|
||||||
|
|
||||||
|
- An `h2` element with the text `Past Events`.
|
||||||
|
- Two `article` elements. Each article element should represent a past event, and it should have:
|
||||||
|
- An `h3` element for the event title,
|
||||||
|
- A `p` element for the event description. You can add a date at the bottom if you like.
|
||||||
|
- An image element with the `src` attribute pointing to an image file and the `alt` attribute with a description of the image.
|
||||||
|
|
||||||
|
**Note:** You can use any text for the event descriptions and dates. You can use the following image URLs for the images if you like:
|
||||||
|
|
||||||
|
- `https://cdn.freecodecamp.org/curriculum/labs/past-event1.jpg`.
|
||||||
|
- `https://cdn.freecodecamp.org/curriculum/labs/past-event2.jpg`.
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
You should have a `header` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.isNotNull(document.querySelector("header"));
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `header` element should come after the opening `body` tag.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.equal(document.querySelector("body")?.firstElementChild?.tagName, "HEADER");
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the `header` element, you should have an `h1` element that contains the text `Event Hub`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const h1Element = document.querySelector('header h1');
|
||||||
|
assert.strictEqual(h1Element?.innerText, "Event Hub");
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the `header` element, after the `h1` element, you should have a `nav` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
assert.isNotNull(document.querySelector("header>h1+nav"));
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `nav` element should contain an unordered list of two items.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const liElements = document.querySelectorAll('header nav>ul>li');
|
||||||
|
|
||||||
|
assert.isNotNull('header nav>ul');
|
||||||
|
assert.strictEqual(liElements.length, 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
The first item in the unordered list should be a link.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const firstLink = document.querySelectorAll('header nav ul li a')[0];
|
||||||
|
assert.exists(firstLink);
|
||||||
|
```
|
||||||
|
|
||||||
|
The second item in the unordered list should be a link.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const secondLink = document.querySelectorAll('header nav ul li a')[1];
|
||||||
|
assert.exists(secondLink);
|
||||||
|
```
|
||||||
|
|
||||||
|
The text of the first item in the unordered list should be `Upcoming Events`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const firstLink = document.querySelectorAll('header nav>ul>li>a')[0];
|
||||||
|
assert.strictEqual(firstLink.innerText, "Upcoming Events");
|
||||||
|
```
|
||||||
|
|
||||||
|
The first item in the unordered list should have the `href` set to `#upcoming-events`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const anchorElement = document.querySelectorAll("header nav>ul>li>a")[0];
|
||||||
|
const hrefAttribute = anchorElement?.getAttribute("href");
|
||||||
|
assert.strictEqual(hrefAttribute, "#upcoming-events");
|
||||||
|
```
|
||||||
|
|
||||||
|
The text of the second item in the unordered list should be `Past Events`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const secondLink = document.querySelectorAll('header nav>ul>li>a')[1];
|
||||||
|
assert.strictEqual(secondLink.innerText, "Past Events");
|
||||||
|
```
|
||||||
|
|
||||||
|
The second item in the unordered list should have the `href` set to `#past-events`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const anchorElement = document.querySelectorAll("header nav>ul>li>a")[1];
|
||||||
|
const hrefAttribute = anchorElement?.getAttribute("href");
|
||||||
|
assert.strictEqual(hrefAttribute, "#past-events");
|
||||||
|
```
|
||||||
|
|
||||||
|
You should have a `main` element after the `header` element closing tag.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const mainElement = document.querySelector("body>header+main");
|
||||||
|
assert.isNotNull(mainElement);
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the `main` element, you should have two `section` elements.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const sectionElements = document.querySelectorAll('body>header+main>section');
|
||||||
|
assert.strictEqual(sectionElements.length, 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
Your first `section` element should have an `id` attribute with the value `upcoming-events`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const firstSection = document.querySelectorAll('body>header+main>section')[0];
|
||||||
|
const idAttribute = firstSection?.getAttribute("id");
|
||||||
|
assert.strictEqual(idAttribute, "upcoming-events");
|
||||||
|
```
|
||||||
|
|
||||||
|
Your second `section` element should have an `id` attribute with the value `past-events`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const secondSection = document.querySelectorAll('body>header+main>section')[1];
|
||||||
|
const idAttribute = secondSection?.getAttribute("id");
|
||||||
|
assert.strictEqual(idAttribute, "past-events");
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the `#upcoming-events` section, you should have an `h2` element with the text `Upcoming Events`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const h2Element = document.querySelector('#upcoming-events h2');
|
||||||
|
assert.strictEqual(h2Element?.innerText, "Upcoming Events");
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the `#upcoming-events` section, you should have two `article` elements below the `h2` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const articleElements = document.querySelectorAll('#upcoming-events h2 ~ article');
|
||||||
|
assert.strictEqual(articleElements.length, 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
Both of the `article` elements inside the `#upcoming-events` section should have an `h3` element for the event title.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const h3Elements = document.querySelectorAll('#upcoming-events article h3');
|
||||||
|
assert.strictEqual(h3Elements.length, 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
Both of the `article` elements inside the `#upcoming-events` section should have a paragraph element for the event description.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const articles = document.querySelectorAll('#upcoming-events article');
|
||||||
|
assert.isNotEmpty(articles);
|
||||||
|
articles.forEach(article => {
|
||||||
|
assert.isAtLeast(article.querySelectorAll('h3 ~ p').length, 1);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the `#past-events` section, you should have an `h2` element with the text `Past Events`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const h2Element = document.querySelector('#past-events h2');
|
||||||
|
assert.strictEqual(h2Element?.innerText, "Past Events");
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the `#past-events` section, you should have two `article` elements below the `h2` element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const articleElements = document.querySelectorAll('#past-events h2 ~ article');
|
||||||
|
assert.strictEqual(articleElements.length, 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
Both of the `article` elements inside the `#past-events` section should have an `h3` element for the event title.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const h3Elements = document.querySelectorAll('#past-events article h3');
|
||||||
|
assert.strictEqual(h3Elements.length, 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
Both of the `article` elements inside the `#past-events` section should have a paragraph element for the event description.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const articles = document.querySelectorAll('#past-events article');
|
||||||
|
assert.isNotEmpty(articles);
|
||||||
|
articles.forEach(article => {
|
||||||
|
assert.isAtLeast(article.querySelectorAll('h3 ~ p').length, 1);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Both of the `article` elements inside the `#past-events` section should have an image element.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const imgElements = document.querySelectorAll('#past-events article img');
|
||||||
|
assert.strictEqual(imgElements.length, 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
Both of the image elements inside the `#past-events` section should have the `src` attribute pointing to an image file.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const imgElements = document.querySelectorAll('#past-events article img');
|
||||||
|
assert.strictEqual(imgElements.length, 2);
|
||||||
|
|
||||||
|
for (let img of imgElements) {
|
||||||
|
assert.exists(img.getAttribute("src"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Both of the image elements inside the `#past-events` section should have the `alt` attribute with a description of the image.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const imgElements = document.querySelectorAll('#past-events article img');
|
||||||
|
assert.strictEqual(imgElements.length, 2);
|
||||||
|
|
||||||
|
for (let img of imgElements) {
|
||||||
|
assert.exists(img.getAttribute("alt"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each `h3` element should have the event title.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const eventTitles = document.querySelectorAll('h3');
|
||||||
|
assert.isNotEmpty(eventTitles);
|
||||||
|
eventTitles.forEach((eventTitle => assert.isNotEmpty(eventTitle.innerText)));
|
||||||
|
```
|
||||||
|
|
||||||
|
Each `p` element should have the event description.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const eventDescriptions = document.querySelectorAll('p');
|
||||||
|
assert.isNotEmpty(eventDescriptions);
|
||||||
|
eventDescriptions.forEach((eventDescription => assert.isNotEmpty(eventDescription.innerText)));
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Event Hub</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
# --solutions--
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Event Hub</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>Event Hub</h1>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#upcoming-events">Upcoming Events</a></li>
|
||||||
|
<li><a href="#past-events">Past Events</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<section id="upcoming-events">
|
||||||
|
<h2>Upcoming Events</h2>
|
||||||
|
<article>
|
||||||
|
<h3>AI & Machine Learning Conference 2024</h3>
|
||||||
|
<p>Join us for a deep dive into the latest advancements in artificial intelligence and machine learning. Industry leaders will share insights and case studies on how AI is transforming various sectors.</p>
|
||||||
|
<p>Date: August 10, 2024</p>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<h3>Web Development Bootcamp</h3>
|
||||||
|
<p>A hands-on workshop designed for developers looking to enhance their skills in modern web technologies including React, Node.js, and GraphQL. Perfect for both beginners and experienced developers.</p>
|
||||||
|
<p>Date: September 5, 2024</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
<section id="past-events">
|
||||||
|
<h2>Past Events</h2>
|
||||||
|
<article>
|
||||||
|
<h3>Cybersecurity Summit 2024</h3>
|
||||||
|
<p>An event focusing on the latest trends and threats in cybersecurity. Experts discussed strategies for protecting data and ensuring privacy in an increasingly digital world.</p>
|
||||||
|
<p>Date: June 15, 2024</p>
|
||||||
|
<img src="https://cdn.freecodecamp.org/curriculum/labs/past-event1.jpg" alt="Image from Cybersecurity Summit 2024">
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<h3>Blockchain Expo 2024</h3>
|
||||||
|
<p>A comprehensive event covering the future of blockchain technology. Topics included decentralized finance (DeFi), smart contracts, and the impact of blockchain on various industries.</p>
|
||||||
|
<p>Date: July 20, 2024</p>
|
||||||
|
<img src="https://cdn.freecodecamp.org/curriculum/labs/past-event2.jpg" alt="Image from Blockchain Expo 2024">
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
@@ -2,7 +2,10 @@ const Joi = require('joi');
|
|||||||
Joi.objectId = require('joi-objectid')(Joi);
|
Joi.objectId = require('joi-objectid')(Joi);
|
||||||
|
|
||||||
const { challengeTypes } = require('../../shared/config/challenge-types');
|
const { challengeTypes } = require('../../shared/config/challenge-types');
|
||||||
const { chapterBasedSuperBlocks } = require('../../shared/config/curriculum');
|
const {
|
||||||
|
chapterBasedSuperBlocks,
|
||||||
|
catalogSuperBlocks
|
||||||
|
} = require('../../shared/config/curriculum');
|
||||||
const {
|
const {
|
||||||
availableCharacters,
|
availableCharacters,
|
||||||
availableBackgrounds,
|
availableBackgrounds,
|
||||||
@@ -128,7 +131,7 @@ const schema = Joi.object()
|
|||||||
block: Joi.string().regex(slugRE).required(),
|
block: Joi.string().regex(slugRE).required(),
|
||||||
blockId: Joi.objectId(),
|
blockId: Joi.objectId(),
|
||||||
blockType: Joi.when('superBlock', {
|
blockType: Joi.when('superBlock', {
|
||||||
is: chapterBasedSuperBlocks,
|
is: [...chapterBasedSuperBlocks, ...catalogSuperBlocks],
|
||||||
then: Joi.valid(
|
then: Joi.valid(
|
||||||
'workshop',
|
'workshop',
|
||||||
'lab',
|
'lab',
|
||||||
|
|||||||
@@ -52,6 +52,18 @@ const duplicatedProjectIds = [
|
|||||||
'5ef9b03c81a63668521804ee',
|
'5ef9b03c81a63668521804ee',
|
||||||
'62bb4009e3458a128ff57d5d',
|
'62bb4009e3458a128ff57d5d',
|
||||||
|
|
||||||
|
// Recipe Page
|
||||||
|
'668f08ea07b99b1f4a91acab',
|
||||||
|
|
||||||
|
// Cat Blog
|
||||||
|
'669aff9f5488f1bea056416d',
|
||||||
|
'669fc7e141e4703748c558bf',
|
||||||
|
'669fc938d38e6e38ace9251e',
|
||||||
|
'669fcb06c3034a39f5431a38',
|
||||||
|
|
||||||
|
// Event hub
|
||||||
|
'66ebd4ae2812430bb883c787',
|
||||||
|
|
||||||
// Survey Form
|
// Survey Form
|
||||||
'587d78af367417b2b2512b03',
|
'587d78af367417b2b2512b03',
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ describe('getSuperBlockFromPath', () => {
|
|||||||
.filter(item => fs.lstatSync(path.join(englishFolder, item)).isDirectory());
|
.filter(item => fs.lstatSync(path.join(englishFolder, item)).isDirectory());
|
||||||
|
|
||||||
it('handles all the directories in ./challenges/english', () => {
|
it('handles all the directories in ./challenges/english', () => {
|
||||||
expect.assertions(27);
|
expect.assertions(29);
|
||||||
|
|
||||||
for (const directory of directories) {
|
for (const directory of directories) {
|
||||||
expect(() => getSuperBlockFromDir(directory)).not.toThrow();
|
expect(() => getSuperBlockFromDir(directory)).not.toThrow();
|
||||||
@@ -186,7 +186,7 @@ describe('getSuperBlockFromPath', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns valid superblocks (or 'certifications') for all valid arguments", () => {
|
it("returns valid superblocks (or 'certifications') for all valid arguments", () => {
|
||||||
expect.assertions(27);
|
expect.assertions(29);
|
||||||
|
|
||||||
const superBlockPaths = directories.filter(x => x !== '00-certifications');
|
const superBlockPaths = directories.filter(x => x !== '00-certifications');
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -281,6 +281,8 @@ export const superBlockToCertMap: {
|
|||||||
[SuperBlocks.ProjectEuler]: null,
|
[SuperBlocks.ProjectEuler]: null,
|
||||||
[SuperBlocks.TheOdinProject]: null,
|
[SuperBlocks.TheOdinProject]: null,
|
||||||
[SuperBlocks.RosettaCode]: null,
|
[SuperBlocks.RosettaCode]: null,
|
||||||
|
[SuperBlocks.BasicHtml]: null,
|
||||||
|
[SuperBlocks.SemanticHtml]: null,
|
||||||
[SuperBlocks.DevPlayground]: null
|
[SuperBlocks.DevPlayground]: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const blocklistedUsernames = [
|
|||||||
'backend-challenge-completed',
|
'backend-challenge-completed',
|
||||||
'blocked',
|
'blocked',
|
||||||
'bonfire',
|
'bonfire',
|
||||||
|
'catalog',
|
||||||
'cats.json',
|
'cats.json',
|
||||||
'challenge-completed',
|
'challenge-completed',
|
||||||
'challenge',
|
'challenge',
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ describe('generateSuperBlockList', () => {
|
|||||||
});
|
});
|
||||||
const tempSuperBlockMap = { ...superBlockStages };
|
const tempSuperBlockMap = { ...superBlockStages };
|
||||||
tempSuperBlockMap[SuperBlockStage.Upcoming] = [];
|
tempSuperBlockMap[SuperBlockStage.Upcoming] = [];
|
||||||
|
tempSuperBlockMap[SuperBlockStage.Catalog] = [];
|
||||||
expect(result).toHaveLength(Object.values(tempSuperBlockMap).flat().length);
|
expect(result).toHaveLength(Object.values(tempSuperBlockMap).flat().length);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export enum SuperBlocks {
|
|||||||
A2Chinese = 'a2-professional-chinese',
|
A2Chinese = 'a2-professional-chinese',
|
||||||
RosettaCode = 'rosetta-code',
|
RosettaCode = 'rosetta-code',
|
||||||
PythonForEverybody = 'python-for-everybody',
|
PythonForEverybody = 'python-for-everybody',
|
||||||
|
BasicHtml = 'basic-html',
|
||||||
|
SemanticHtml = 'semantic-html',
|
||||||
DevPlayground = 'dev-playground'
|
DevPlayground = 'dev-playground'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +63,8 @@ export const superBlockToFolderMap = {
|
|||||||
[SuperBlocks.FullStackDeveloper]: '25-front-end-development',
|
[SuperBlocks.FullStackDeveloper]: '25-front-end-development',
|
||||||
[SuperBlocks.A2Spanish]: '26-a2-professional-spanish',
|
[SuperBlocks.A2Spanish]: '26-a2-professional-spanish',
|
||||||
[SuperBlocks.A2Chinese]: '27-a2-professional-chinese',
|
[SuperBlocks.A2Chinese]: '27-a2-professional-chinese',
|
||||||
|
[SuperBlocks.BasicHtml]: '28-basic-html',
|
||||||
|
[SuperBlocks.SemanticHtml]: '29-semantic-html',
|
||||||
[SuperBlocks.DevPlayground]: '99-dev-playground'
|
[SuperBlocks.DevPlayground]: '99-dev-playground'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,7 +95,8 @@ export enum SuperBlockStage {
|
|||||||
Extra,
|
Extra,
|
||||||
Legacy,
|
Legacy,
|
||||||
Upcoming,
|
Upcoming,
|
||||||
Next
|
Next,
|
||||||
|
Catalog
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultStageOrder = [
|
const defaultStageOrder = [
|
||||||
@@ -108,7 +113,9 @@ export function getStageOrder({
|
|||||||
}: Config): SuperBlockStage[] {
|
}: Config): SuperBlockStage[] {
|
||||||
const stageOrder = [...defaultStageOrder];
|
const stageOrder = [...defaultStageOrder];
|
||||||
|
|
||||||
if (showUpcomingChanges) stageOrder.push(SuperBlockStage.Upcoming);
|
if (showUpcomingChanges) {
|
||||||
|
stageOrder.push(SuperBlockStage.Upcoming, SuperBlockStage.Catalog);
|
||||||
|
}
|
||||||
return stageOrder;
|
return stageOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +126,6 @@ export type StageMap = {
|
|||||||
// Groups of superblocks in learn map. This should include all superblocks.
|
// Groups of superblocks in learn map. This should include all superblocks.
|
||||||
export const superBlockStages: StageMap = {
|
export const superBlockStages: StageMap = {
|
||||||
[SuperBlockStage.Core]: [SuperBlocks.FullStackDeveloper],
|
[SuperBlockStage.Core]: [SuperBlocks.FullStackDeveloper],
|
||||||
|
|
||||||
[SuperBlockStage.English]: [SuperBlocks.A2English, SuperBlocks.B1English],
|
[SuperBlockStage.English]: [SuperBlocks.A2English, SuperBlocks.B1English],
|
||||||
[SuperBlockStage.Professional]: [SuperBlocks.FoundationalCSharp],
|
[SuperBlockStage.Professional]: [SuperBlocks.FoundationalCSharp],
|
||||||
[SuperBlockStage.Extra]: [
|
[SuperBlockStage.Extra]: [
|
||||||
@@ -150,11 +156,16 @@ export const superBlockStages: StageMap = {
|
|||||||
SuperBlocks.A2Spanish,
|
SuperBlocks.A2Spanish,
|
||||||
SuperBlocks.A2Chinese,
|
SuperBlocks.A2Chinese,
|
||||||
SuperBlocks.DevPlayground
|
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);
|
Object.freeze(superBlockStages);
|
||||||
|
|
||||||
|
export const catalogSuperBlocks = superBlockStages[SuperBlockStage.Catalog];
|
||||||
|
|
||||||
type NotAuditedSuperBlocks = {
|
type NotAuditedSuperBlocks = {
|
||||||
[key in Languages]: SuperBlocks[];
|
[key in Languages]: SuperBlocks[];
|
||||||
};
|
};
|
||||||
@@ -178,6 +189,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.A2Spanish,
|
SuperBlocks.A2Spanish,
|
||||||
SuperBlocks.A2Chinese,
|
SuperBlocks.A2Chinese,
|
||||||
SuperBlocks.PythonForEverybody,
|
SuperBlocks.PythonForEverybody,
|
||||||
|
SuperBlocks.BasicHtml,
|
||||||
|
SuperBlocks.SemanticHtml,
|
||||||
SuperBlocks.DevPlayground
|
SuperBlocks.DevPlayground
|
||||||
],
|
],
|
||||||
[Languages.Chinese]: [
|
[Languages.Chinese]: [
|
||||||
@@ -190,6 +203,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.A2Spanish,
|
SuperBlocks.A2Spanish,
|
||||||
SuperBlocks.A2Chinese,
|
SuperBlocks.A2Chinese,
|
||||||
SuperBlocks.PythonForEverybody,
|
SuperBlocks.PythonForEverybody,
|
||||||
|
SuperBlocks.BasicHtml,
|
||||||
|
SuperBlocks.SemanticHtml,
|
||||||
SuperBlocks.DevPlayground
|
SuperBlocks.DevPlayground
|
||||||
],
|
],
|
||||||
[Languages.ChineseTraditional]: [
|
[Languages.ChineseTraditional]: [
|
||||||
@@ -202,6 +217,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.A2Spanish,
|
SuperBlocks.A2Spanish,
|
||||||
SuperBlocks.A2Chinese,
|
SuperBlocks.A2Chinese,
|
||||||
SuperBlocks.PythonForEverybody,
|
SuperBlocks.PythonForEverybody,
|
||||||
|
SuperBlocks.BasicHtml,
|
||||||
|
SuperBlocks.SemanticHtml,
|
||||||
SuperBlocks.DevPlayground
|
SuperBlocks.DevPlayground
|
||||||
],
|
],
|
||||||
[Languages.Italian]: [
|
[Languages.Italian]: [
|
||||||
@@ -214,6 +231,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.A2Spanish,
|
SuperBlocks.A2Spanish,
|
||||||
SuperBlocks.A2Chinese,
|
SuperBlocks.A2Chinese,
|
||||||
SuperBlocks.PythonForEverybody,
|
SuperBlocks.PythonForEverybody,
|
||||||
|
SuperBlocks.BasicHtml,
|
||||||
|
SuperBlocks.SemanticHtml,
|
||||||
SuperBlocks.DevPlayground
|
SuperBlocks.DevPlayground
|
||||||
],
|
],
|
||||||
[Languages.Portuguese]: [
|
[Languages.Portuguese]: [
|
||||||
@@ -224,6 +243,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.A2Spanish,
|
SuperBlocks.A2Spanish,
|
||||||
SuperBlocks.A2Chinese,
|
SuperBlocks.A2Chinese,
|
||||||
SuperBlocks.PythonForEverybody,
|
SuperBlocks.PythonForEverybody,
|
||||||
|
SuperBlocks.BasicHtml,
|
||||||
|
SuperBlocks.SemanticHtml,
|
||||||
SuperBlocks.DevPlayground
|
SuperBlocks.DevPlayground
|
||||||
],
|
],
|
||||||
[Languages.Ukrainian]: [
|
[Languages.Ukrainian]: [
|
||||||
@@ -233,6 +254,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.B1English,
|
SuperBlocks.B1English,
|
||||||
SuperBlocks.A2Spanish,
|
SuperBlocks.A2Spanish,
|
||||||
SuperBlocks.A2Chinese,
|
SuperBlocks.A2Chinese,
|
||||||
|
SuperBlocks.BasicHtml,
|
||||||
|
SuperBlocks.SemanticHtml,
|
||||||
SuperBlocks.DevPlayground
|
SuperBlocks.DevPlayground
|
||||||
],
|
],
|
||||||
[Languages.Japanese]: [
|
[Languages.Japanese]: [
|
||||||
@@ -243,6 +266,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.B1English,
|
SuperBlocks.B1English,
|
||||||
SuperBlocks.A2Spanish,
|
SuperBlocks.A2Spanish,
|
||||||
SuperBlocks.A2Chinese,
|
SuperBlocks.A2Chinese,
|
||||||
|
SuperBlocks.BasicHtml,
|
||||||
|
SuperBlocks.SemanticHtml,
|
||||||
SuperBlocks.DevPlayground
|
SuperBlocks.DevPlayground
|
||||||
],
|
],
|
||||||
[Languages.German]: [
|
[Languages.German]: [
|
||||||
@@ -262,6 +287,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.A2Spanish,
|
SuperBlocks.A2Spanish,
|
||||||
SuperBlocks.A2Chinese,
|
SuperBlocks.A2Chinese,
|
||||||
SuperBlocks.PythonForEverybody,
|
SuperBlocks.PythonForEverybody,
|
||||||
|
SuperBlocks.BasicHtml,
|
||||||
|
SuperBlocks.SemanticHtml,
|
||||||
SuperBlocks.DevPlayground
|
SuperBlocks.DevPlayground
|
||||||
],
|
],
|
||||||
[Languages.Swahili]: [
|
[Languages.Swahili]: [
|
||||||
@@ -288,6 +315,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.A2Spanish,
|
SuperBlocks.A2Spanish,
|
||||||
SuperBlocks.A2Chinese,
|
SuperBlocks.A2Chinese,
|
||||||
SuperBlocks.PythonForEverybody,
|
SuperBlocks.PythonForEverybody,
|
||||||
|
SuperBlocks.BasicHtml,
|
||||||
|
SuperBlocks.SemanticHtml,
|
||||||
SuperBlocks.DevPlayground
|
SuperBlocks.DevPlayground
|
||||||
],
|
],
|
||||||
[Languages.Korean]: [
|
[Languages.Korean]: [
|
||||||
@@ -315,6 +344,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.DataVis,
|
SuperBlocks.DataVis,
|
||||||
SuperBlocks.RelationalDb,
|
SuperBlocks.RelationalDb,
|
||||||
SuperBlocks.RosettaCode,
|
SuperBlocks.RosettaCode,
|
||||||
|
SuperBlocks.BasicHtml,
|
||||||
|
SuperBlocks.SemanticHtml,
|
||||||
SuperBlocks.DevPlayground
|
SuperBlocks.DevPlayground
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -98,5 +98,13 @@ export const superBlockList = [
|
|||||||
{
|
{
|
||||||
name: 'A2 Professional Chinese (Beta)',
|
name: 'A2 Professional Chinese (Beta)',
|
||||||
path: '27-a2-professional-chinese'
|
path: '27-a2-professional-chinese'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Basic HTML',
|
||||||
|
path: '28-basic-html'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Semantic HTML',
|
||||||
|
path: '29-semantic-html'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -141,7 +141,9 @@ ${result.error.message}`);
|
|||||||
.filter(([key]) => {
|
.filter(([key]) => {
|
||||||
const stage = Number(key) as SuperBlockStage;
|
const stage = Number(key) as SuperBlockStage;
|
||||||
return (
|
return (
|
||||||
stage !== SuperBlockStage.Next && stage !== SuperBlockStage.Upcoming
|
stage !== SuperBlockStage.Next &&
|
||||||
|
stage !== SuperBlockStage.Upcoming &&
|
||||||
|
stage !== SuperBlockStage.Catalog
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.flatMap(([, superBlocks]) => superBlocks);
|
.flatMap(([, superBlocks]) => superBlocks);
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ describe('external curriculum data build', () => {
|
|||||||
test('the available-superblocks file should have the correct structure', async () => {
|
test('the available-superblocks file should have the correct structure', async () => {
|
||||||
const filteredSuperBlockStages: string[] = Object.keys(SuperBlockStage)
|
const filteredSuperBlockStages: string[] = Object.keys(SuperBlockStage)
|
||||||
.filter(key => isNaN(Number(key))) // Filter out numeric keys to get only the names
|
.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());
|
.map(name => name.toLowerCase());
|
||||||
|
|
||||||
const validateAvailableSuperBlocks = availableSuperBlocksValidator();
|
const validateAvailableSuperBlocks = availableSuperBlocksValidator();
|
||||||
|
|||||||
Reference in New Issue
Block a user