diff --git a/client/i18n/locales/english/intro.json b/client/i18n/locales/english/intro.json index 3f652f31afa..78f6af9c555 100644 --- a/client/i18n/locales/english/intro.json +++ b/client/i18n/locales/english/intro.json @@ -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 main, nav, article and footer 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": { diff --git a/client/src/assets/superblock-icon.tsx b/client/src/assets/superblock-icon.tsx index ff2b448e3b2..21f1dd2a4a4 100644 --- a/client/src/assets/superblock-icon.tsx +++ b/client/src/assets/superblock-icon.tsx @@ -50,6 +50,7 @@ const iconMap = { [SuperBlocks.PythonForEverybody]: PythonIcon, [SuperBlocks.BasicHtml]: Code, [SuperBlocks.SemanticHtml]: Code, + [SuperBlocks.FullStackOpen]: Code, [SuperBlocks.DevPlayground]: Code }; diff --git a/client/src/pages/learn/full-stack-open/index.md b/client/src/pages/learn/full-stack-open/index.md new file mode 100644 index 00000000000..1879f8718fd --- /dev/null +++ b/client/src/pages/learn/full-stack-open/index.md @@ -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. diff --git a/client/src/templates/Introduction/components/super-block-accordion.tsx b/client/src/templates/Introduction/components/super-block-accordion.tsx index d1f43072d0a..e33d60813b0 100644 --- a/client/src/templates/Introduction/components/super-block-accordion.tsx +++ b/client/src/templates/Introduction/components/super-block-accordion.tsx @@ -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( - ({ 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(); - chapters.forEach(chapter => { - chapter.modules.forEach(module => { - module.blocks.forEach(block => { - blockToChapterMap.set(block, chapter.dashedName); - }); - }); - }); - - return blockToChapterMap; -}; - -const getBlockToModuleMap = () => { - const blockToModuleMap = new Map(); - 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( + ({ modules }) => modules + ); + + const isLinkModule = (name: string) => { + const module = modules.find(module => module.dashedName === name); + + return module?.moduleType === 'review'; + }; + + const getBlockToChapterMap = () => { + const blockToChapterMap = new Map(); + 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(); + 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); diff --git a/curriculum/build-curriculum.js b/curriculum/build-curriculum.js index a4d4c006ef5..dff383f3a88 100644 --- a/curriculum/build-curriculum.js +++ b/curriculum/build-curriculum.js @@ -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( diff --git a/curriculum/structure/curriculum.json b/curriculum/structure/curriculum.json index a86bf497147..164a30297e6 100644 --- a/curriculum/structure/curriculum.json +++ b/curriculum/structure/curriculum.json @@ -28,7 +28,8 @@ "basic-html", "semantic-html", "a1-professional-chinese", - "dev-playground" + "dev-playground", + "full-stack-open" ], "certifications": [ "a2-english-for-developers", diff --git a/curriculum/structure/superblocks/full-stack-open.json b/curriculum/structure/superblocks/full-stack-open.json new file mode 100644 index 00000000000..145f372985f --- /dev/null +++ b/curriculum/structure/superblocks/full-stack-open.json @@ -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"] + } + ] + } + ] +} diff --git a/shared/config/certification-settings.ts b/shared/config/certification-settings.ts index 601b23ee9a7..05af8e5711b 100644 --- a/shared/config/certification-settings.ts +++ b/shared/config/certification-settings.ts @@ -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]; diff --git a/shared/config/curriculum.ts b/shared/config/curriculum.ts index de0332a8614..708a0765bd6 100644 --- a/shared/config/curriculum.ts +++ b/shared/config/curriculum.ts @@ -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 = {