feat(curriculum): add Full Stack Open Skeleton (#61974)

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
Sem Bauke
2025-09-03 15:43:45 +02:00
committed by GitHub
parent 3aced6dc6a
commit 041074b7bc
9 changed files with 271 additions and 45 deletions

View File

@@ -4898,6 +4898,45 @@
}
}
},
"full-stack-open": {
"title": "Full Stack Open",
"intro": ["A good intro is to be added here."],
"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."
]
}
},
"chapters": {
"part-0": "Fundamentals of Web Apps",
"part-1": "Introduction to React",
"part-2": "Communicating with Servers",
"part-3": "Programming a Server with NodeJS and Express",
"part-4": "Testing Express Servers, User Administration",
"part-5": "Testing React Apps",
"part-6": "Advanced State Management",
"part-7": "React router, custom hooks, styling app with CSS and webpack",
"part-8": "GraphQL",
"part-9": "TypeScript",
"part-10": "React Native",
"part-11": "CI/CD",
"part-12": "Containers",
"part-13": "Using Relational Databases"
},
"modules": {
"basic-html": "Basic HTML"
},
"module-intros": {
"basic-html": {
"title": "Basic HTML",
"intro": [
"Learn how to build simple webpages using HTML tags to add text, images, and links."
]
}
}
},
"daily-coding-challenge": {
"title": "Daily Coding Challenge",
"blocks": {

View File

@@ -50,6 +50,7 @@ const iconMap = {
[SuperBlocks.PythonForEverybody]: PythonIcon,
[SuperBlocks.BasicHtml]: Code,
[SuperBlocks.SemanticHtml]: Code,
[SuperBlocks.FullStackOpen]: Code,
[SuperBlocks.DevPlayground]: Code
};

View File

@@ -0,0 +1,9 @@
---
title: Full Stack Open
superBlock: full-stack-open
certification: full-stack-open
---
## Full Stack Open
The [Full Stack Open](https://fullstackopen.com/en/) curriculum, created by the University of Helsinki, covers modern web development with JavaScript. The curriculum focuses on building single page applications with ReactJS that use REST APIs built with Node.js. It also covers GraphQL APIs, TypeScript, and testing.

View File

@@ -7,7 +7,9 @@ import { SuperBlocks } from '../../../../../shared/config/curriculum';
import DropDown from '../../../assets/icons/dropdown';
// TODO: source the superblock structure via a GQL query, rather than directly
// from the curriculum
import superBlockStructure from '../../../../../curriculum/structure/superblocks/full-stack-developer.json';
import fullStackCert from '../../../../../curriculum/structure/superblocks/full-stack-developer.json';
import fullStackOpen from '../../../../../curriculum/structure/superblocks/full-stack-open.json';
import { ChapterIcon } from '../../../assets/chapter-icon';
import { BlockLayouts, BlockTypes } from '../../../../../shared/config/blocks';
import { FsdChapters } from '../../../../../shared/config/chapters';
@@ -58,44 +60,6 @@ interface SuperBlockAccordionProps {
completedChallengeIds: string[];
}
const modules = superBlockStructure.chapters.flatMap<Module>(
({ modules }) => modules
);
const chapters = superBlockStructure.chapters;
const isLinkModule = (name: string) => {
const module = modules.find(module => module.dashedName === name);
return module?.moduleType === 'review';
};
const getBlockToChapterMap = () => {
const blockToChapterMap = new Map<string, string>();
chapters.forEach(chapter => {
chapter.modules.forEach(module => {
module.blocks.forEach(block => {
blockToChapterMap.set(block, chapter.dashedName);
});
});
});
return blockToChapterMap;
};
const getBlockToModuleMap = () => {
const blockToModuleMap = new Map<string, string>();
modules.forEach(module => {
module.blocks.forEach(block => {
blockToModuleMap.set(block, module.dashedName);
});
});
return blockToModuleMap;
};
const blockToChapterMap = getBlockToChapterMap();
const blockToModuleMap = getBlockToModuleMap();
const Chapter = ({
dashedName,
children,
@@ -210,8 +174,59 @@ export const SuperBlockAccordion = ({
chosenBlock,
completedChallengeIds
}: SuperBlockAccordionProps) => {
function getSuperblockStructure(superBlock: SuperBlocks) {
switch (superBlock) {
case SuperBlocks.FullStackOpen:
return fullStackOpen;
case SuperBlocks.FullStackDeveloper:
return fullStackCert;
default:
throw new Error("The SuperBlock structure hasn't been imported.");
}
}
const superBlockStructure = getSuperblockStructure(superBlock);
const modules = superBlockStructure.chapters.flatMap<Module>(
({ modules }) => modules
);
const isLinkModule = (name: string) => {
const module = modules.find(module => module.dashedName === name);
return module?.moduleType === 'review';
};
const getBlockToChapterMap = () => {
const blockToChapterMap = new Map<string, string>();
superBlockStructure.chapters.forEach(chapter => {
chapter.modules.forEach(module => {
module.blocks.forEach(block => {
blockToChapterMap.set(block, chapter.dashedName);
});
});
});
return blockToChapterMap;
};
const getBlockToModuleMap = () => {
const blockToModuleMap = new Map<string, string>();
modules.forEach(module => {
module.blocks.forEach(block => {
blockToModuleMap.set(block, module.dashedName);
});
});
return blockToModuleMap;
};
const blockToChapterMap = getBlockToChapterMap();
const blockToModuleMap = getBlockToModuleMap();
const { t } = useTranslation();
const { allChapters } = useMemo(() => {
const chapters = superBlockStructure.chapters;
const populateBlocks = (blocks: string[]) =>
blocks.map(block => {
const blockChallenges = challenges.filter(
@@ -237,7 +252,7 @@ export const SuperBlockAccordion = ({
}));
return { allChapters };
}, [challenges]);
}, [challenges, superBlockStructure.chapters]);
// Expand the outer layers in order to reveal the chosen block.
const expandedChapter = blockToChapterMap.get(chosenBlock);

View File

@@ -187,7 +187,8 @@ const superBlockNames = {
'basic-html': 'basic-html',
'semantic-html': 'semantic-html',
'a1-professional-chinese': 'a1-professional-chinese',
'dev-playground': 'dev-playground'
'dev-playground': 'dev-playground',
'full-stack-open': 'full-stack-open'
};
const superBlockToFilename = Object.entries(superBlockNames).reduce(

View File

@@ -28,7 +28,8 @@
"basic-html",
"semantic-html",
"a1-professional-chinese",
"dev-playground"
"dev-playground",
"full-stack-open"
],
"certifications": [
"a2-english-for-developers",

View File

@@ -0,0 +1,154 @@
{
"chapters": [
{
"dashedName": "part-0",
"modules": [
{
"dashedName": "basic-html",
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-1",
"modules": [
{
"dashedName": "basic-html",
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-2",
"comingSoon": true,
"modules": [
{
"dashedName": "basic-html",
"comingSoon": true,
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-3",
"comingSoon": true,
"modules": [
{
"dashedName": "basic-html",
"comingSoon": true,
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-4",
"comingSoon": true,
"modules": [
{
"dashedName": "basic-html",
"comingSoon": true,
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-5",
"comingSoon": true,
"modules": [
{
"dashedName": "basic-html",
"comingSoon": true,
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-6",
"comingSoon": true,
"modules": [
{
"dashedName": "basic-html",
"comingSoon": true,
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-7",
"comingSoon": true,
"modules": [
{
"dashedName": "basic-html",
"comingSoon": true,
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-8",
"comingSoon": true,
"modules": [
{
"dashedName": "basic-html",
"comingSoon": true,
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-9",
"comingSoon": true,
"modules": [
{
"dashedName": "basic-html",
"comingSoon": true,
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-10",
"comingSoon": true,
"modules": [
{
"dashedName": "basic-html",
"comingSoon": true,
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-11",
"comingSoon": true,
"modules": [
{
"dashedName": "basic-html",
"comingSoon": true,
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-12",
"comingSoon": true,
"modules": [
{
"dashedName": "basic-html",
"comingSoon": true,
"blocks": ["cat-blog-page"]
}
]
},
{
"dashedName": "part-13",
"comingSoon": true,
"modules": [
{
"dashedName": "basic-html",
"comingSoon": true,
"blocks": ["cat-blog-page"]
}
]
}
]
}

View File

@@ -286,7 +286,8 @@ export const superBlockToCertMap: {
[SuperBlocks.RosettaCode]: null,
[SuperBlocks.BasicHtml]: null,
[SuperBlocks.SemanticHtml]: null,
[SuperBlocks.DevPlayground]: null
[SuperBlocks.DevPlayground]: null,
[SuperBlocks.FullStackOpen]: null
};
export type CertSlug = (typeof Certification)[keyof typeof Certification];

View File

@@ -32,7 +32,8 @@ export enum SuperBlocks {
PythonForEverybody = 'python-for-everybody',
BasicHtml = 'basic-html',
SemanticHtml = 'semantic-html',
DevPlayground = 'dev-playground'
DevPlayground = 'dev-playground',
FullStackOpen = 'full-stack-open'
}
export const languageSuperBlocks = [
@@ -115,6 +116,7 @@ export const superBlockStages: StageMap = {
],
[SuperBlockStage.Next]: [],
[SuperBlockStage.Upcoming]: [
SuperBlocks.FullStackOpen,
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.A1Chinese,
@@ -325,7 +327,10 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
Object.freeze(notAuditedSuperBlocks);
export const chapterBasedSuperBlocks = [SuperBlocks.FullStackDeveloper];
export const chapterBasedSuperBlocks = [
SuperBlocks.FullStackDeveloper,
SuperBlocks.FullStackOpen
];
Object.freeze(chapterBasedSuperBlocks);
type Config = {