diff --git a/.eslintignore b/.eslintignore index 54d4fb64c7a..6c2b7b2978e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,6 +7,5 @@ shared/config/i18n.js shared/config/certification-settings.js shared/config/donation-settings.js shared/config/superblocks.js -web/** docs/**/*.md playwright*.config.ts diff --git a/.eslintrc.json b/.eslintrc.json index 1b7d143d20e..370a74ef275 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -70,8 +70,6 @@ "./api/tsconfig.json", "./shared/tsconfig.json", "./tools/client-plugins/browser-scripts/tsconfig.json", - "./web/tsconfig.json", - "./curriculum-server/tsconfig.json", "./cypress/tsconfig.json", "./e2e/tsconfig.json" ] @@ -111,10 +109,6 @@ "@typescript-eslint/no-unsafe-assignment": "off" } }, - { - "files": ["web/**/*.tsx"], - "extends": ["plugin:react/jsx-runtime"] - }, { "files": ["**/api-server/**/*", "**/404.*"], "rules": { diff --git a/.github/workflows/node.js-tests.yml b/.github/workflows/node.js-tests.yml index 23e32445cf4..cbb9da537b7 100644 --- a/.github/workflows/node.js-tests.yml +++ b/.github/workflows/node.js-tests.yml @@ -71,8 +71,6 @@ jobs: run: | echo pnpm version $(pnpm -v) pnpm run create:shared - npm i --prefix=curriculum-server - npm i --prefix=web pnpm run build:curriculum pnpm run lint diff --git a/.prettierignore b/.prettierignore index 1fdd5ea226c..ebcc37a5835 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,7 +6,6 @@ client/**/trending.json client/config/*.json client/config/browser-scripts/*.json client/static -curriculum-server/data/curriculum.json curriculum/challenges/_meta/*/* curriculum/challenges/**/* docs/**/*.md @@ -19,4 +18,3 @@ shared/utils/get-lines.test.js shared/utils/is-audited.js shared/utils/validate.js shared/utils/validate.test.js -web/.next diff --git a/curriculum-server/.gitignore b/curriculum-server/.gitignore deleted file mode 100644 index 1269488f7fb..00000000000 --- a/curriculum-server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -data diff --git a/curriculum-server/package.json b/curriculum-server/package.json deleted file mode 100644 index d41bd2ed468..00000000000 --- a/curriculum-server/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@freecodecamp/curriculum-server", - "version": "0.0.1", - "description": "Web server for curriculum data", - "license": "BSD-3-Clause", - "private": true, - "engines": { - "node": ">=16", - "pnpm": ">=9" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git" - }, - "bugs": { - "url": "https://github.com/freeCodeCamp/freeCodeCamp/issues" - }, - "homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme", - "author": "freeCodeCamp ", - "main": "none", - "scripts": { - "dev": "json-server --watch ./data/curriculum.json", - "test": "echo \"Error: no test specified\" && exit 1", - "source-curriculum": "ts-node source-curriculum.ts" - }, - "dependencies": { - "json-server": "0.17.4" - }, - "devDependencies": { - "ts-node": "10.9.2", - "typescript": "5.2.2" - } -} diff --git a/curriculum-server/source-curriculum.ts b/curriculum-server/source-curriculum.ts deleted file mode 100644 index cbea0cf9fca..00000000000 --- a/curriculum-server/source-curriculum.ts +++ /dev/null @@ -1,18 +0,0 @@ -import fs from 'fs/promises'; - -import curriculum from '../shared/config/curriculum.json'; - -interface Curriculum { - [key: string]: unknown; -} -const typedCurriculum = curriculum as Curriculum; - -const patchedCurriculum = Object.keys(typedCurriculum).reduce((acc, key) => { - return { ...acc, [key.replace(/\//g, '-')]: typedCurriculum[key] }; -}, {}); - -void fs - .mkdir('data', { recursive: true }) - .then(() => - fs.writeFile('./data/curriculum.json', JSON.stringify(patchedCurriculum)) - ); diff --git a/curriculum-server/tsconfig.json b/curriculum-server/tsconfig.json deleted file mode 100644 index 4ec0b8780b8..00000000000 --- a/curriculum-server/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "include": ["**/*.ts"], - "exclude": ["node_modules"], - "extends": "../tsconfig-base.json" -} diff --git a/web/.gitignore b/web/.gitignore deleted file mode 100644 index a680367ef56..00000000000 --- a/web/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.next diff --git a/web/README.md b/web/README.md deleted file mode 100644 index 85fc7137dea..00000000000 --- a/web/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Getting Started - -Step 1 : Install freeCodeCamp properly on your system. - -Step 2 : Install the curriculum server and the current package. - -Step 3 : The prepare script will take care of the rest. - -```sh -cd ../curriculum-server -npm i -cd ../web -npm i -pnpm run dev -``` - -Now the server should be running on port 3000 and the client on port 8000. - -For now there's not much to see. - -http://localhost:8000/learn/special-path - -is the main entry point and - -http://localhost:3000/responsive-web-design - -is the curriculum data that is currently being used. - -## Things of Note - -Incremental static regeneration is working quite nicely. You can modify the curriculum data (in /curriculum-server/data/curriculum.json), refresh/reload your browser and the changes will be reflected. - -The trailing ids are a bit buggy, but you can replace them with a new page's mongo id and it will refresh. - -Also, mangled paths _mostly_ work. For example: - -http://localhost:8000/learn/responsive-web-design/applied-an-element/587d774e367417b2b2512a9f - -redirects you to - -http://localhost:8000/learn/responsive-web-design/applied-accessibility/jump-straight-to-the-content-using-the-main-element/587d774e367417b2b2512a9f - -but not all paths behave as desired. diff --git a/web/next-env.d.ts b/web/next-env.d.ts deleted file mode 100644 index 4f11a03dc6c..00000000000 --- a/web/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/web/package.json b/web/package.json deleted file mode 100644 index 236e880edd4..00000000000 --- a/web/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@freecodecamp/web", - "version": "0.0.1", - "description": "The freeCodeCamp.org open-source codebase and curriculum", - "license": "BSD-3-Clause", - "private": true, - "engines": { - "node": ">=16", - "pnpm": ">=9" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git" - }, - "bugs": { - "url": "https://github.com/freeCodeCamp/freeCodeCamp/issues" - }, - "homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme", - "author": "freeCodeCamp ", - "main": "none", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "develop-client": "next dev --port 8000", - "dev": "concurrently \"npm:develop-curriculum-server\" \"npm:develop-client\"", - "develop-curriculum-server": "npm --prefix ../curriculum-server run dev", - "prepare": "npm --prefix ../ run build:curriculum && npm --prefix ../curriculum-server/ run source-curriculum" - }, - "dependencies": { - "@monaco-editor/react": "4.6.0", - "next": "12.3.4", - "react": "18.2.0", - "react-dom": "18.2.0" - }, - "devDependencies": { - "concurrently": "7.6.0" - } -} diff --git a/web/src/data-fetching/get-curriculum.ts b/web/src/data-fetching/get-curriculum.ts deleted file mode 100644 index 52f6ed86ebd..00000000000 --- a/web/src/data-fetching/get-curriculum.ts +++ /dev/null @@ -1,165 +0,0 @@ -/* - Eventually this module could be used as a cache (in memory or on-disk) for the - curriculum, but for now it just fetches the data on demand. - - A reasonably reliable approach would be as follows: - 1. Query the curriculum API for the latest version of the curriculum. - 2. If the API responds - a. If the latest version is not cached, query the API for the latest version - b. Otherwise use the cached value. - 3. If the API does not respond, use the cached value and log an error. - -*/ - -// TODO: this should be { [superblock: string]: Superblock } -export interface Curriculum { - rwdBlocks: SuperBlock; - jsBlocks: SuperBlock; -} - -export interface SuperBlock { - [index: string]: Block; -} - -export interface Block { - meta: { - name: string; - isUpcomingChange: boolean; - dashedName: string; - order: number; - time: string; - template: string; - required: string[]; - superBlock: string; - challengeOrder: { id: string; title: string }[]; - }; - challenges: Challenge[]; -} - -export interface Challenge { - id: string; - dashedName: string; - description: string; - challengeFiles: { contents: string; ext: string }[]; -} - -export interface PathSegments { - superblock: string; - block?: string; - dashedName?: string; - id: string; -} - -export interface IdToDashedNameMap { - [id: string]: string; -} - -interface SuperBlockToChallengeMap { - [index: string]: (pathSegments: Required) => Challenge; -} - -export async function getCurriculum() { - const rwd = await fetch('http://localhost:3000/responsive-web-design'); - const js = await fetch( - 'http://localhost:3000/javascript-algorithms-and-data-structures' - ); - const rwdBlocks = ((await rwd.json()) as { blocks: SuperBlock }).blocks; - const jsBlocks = ((await js.json()) as { blocks: SuperBlock }).blocks; - - return { rwdBlocks, jsBlocks }; -} - -export function getIdToPathSegmentsMap({ rwdBlocks }: Curriculum) { - // TODO: this is pretty inefficient. The curriculum server needs to return an - // object with ids as keys and the superblock, block and dashedName as values. - // i.e. enough info to recreate the full path. - - // Also TODO: use params here and, instead of passing the map, just pass the - // new path. - const idToPathSegmentsMap: Record = {}; - for (const blockName of Object.keys(rwdBlocks)) { - const block = rwdBlocks[blockName]; - for (const challenge of block.challenges) { - idToPathSegmentsMap[challenge.id] = { - superblock: 'responsive-web-design', - block: blockName, - dashedName: challenge.dashedName, - id: challenge.id - }; - } - } - idToPathSegmentsMap['special-path'] = { - superblock: 'responsive-web-design', - id: 'special-path' - }; - return idToPathSegmentsMap; -} - -type SuperBlockToBlockMap = { - [superblock: string]: string[]; -}; - -export function getSuperBlockToBlockMap( - curriculum: Curriculum -): SuperBlockToBlockMap { - return { 'responsive-web-design': Object.keys(curriculum.rwdBlocks) }; -} - -export function getBlockNameToChallengeOrderMap( - { rwdBlocks }: Curriculum, - blockNames: string[] -): { [index: string]: { id: string; title: string } } { - return blockNames.reduce( - (prev, blockName) => ({ - ...prev, - ...{ [blockName]: rwdBlocks[blockName].meta.challengeOrder } - }), - {} - ); -} - -// TODO: remove the hardcoding of superblock names. Also, the map generation is -// a mess -export function getChallengeData( - { rwdBlocks, jsBlocks }: Curriculum, - pathSegments: Required -) { - const superBlockToChallengeMap: SuperBlockToChallengeMap = { - 'responsive-web-design': (pathSegments: Required) => - findChallenge(findBlock(rwdBlocks, pathSegments), pathSegments), - 'javascript-algorithms-and-data-structures': ( - pathSegments: Required - ) => findChallenge(findBlock(jsBlocks, pathSegments), pathSegments) - }; - return superBlockToChallengeMap[pathSegments.superblock](pathSegments); -} - -function findBlock(superblock: SuperBlock, params: Required) { - return superblock[params.block]; -} - -function findChallenge(block: Block, params: PathSegments) { - const challenge = block.challenges.find( - (c: { dashedName: string }) => c.dashedName == params.dashedName - ); - // TODO: is there a nicer way to handle missing challenges? - if (!challenge) { - throw new Error(`Challenge not found: ${params.id}`); - } - return challenge; -} - -// TODO: again, bit ugly. Would be better to get data in this shape from the -// curriculum server. -export function getIdToDashedNameMap({ - rwdBlocks -}: Curriculum): IdToDashedNameMap { - const idToDashedNameMap: Record = {}; - for (const blockName of Object.keys(rwdBlocks)) { - const block = rwdBlocks[blockName]; - for (const challenge of block.challenges) { - idToDashedNameMap[challenge.id] = challenge.dashedName; - } - } - return idToDashedNameMap; -} diff --git a/web/src/page-templates/challenge.tsx b/web/src/page-templates/challenge.tsx deleted file mode 100644 index b0f393271ed..00000000000 --- a/web/src/page-templates/challenge.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { InferGetStaticPropsType } from 'next'; -import { useRouter } from 'next/router'; -import Editor from '@monaco-editor/react'; -import Link from 'next/link'; - -import type { - getStaticProps, - Challenge -} from '../pages/learn/[superblock]/[blockOrId]/[dashedName]/[id]'; - -export default function ChallengeComponent({ - challengeData -}: InferGetStaticPropsType) { - const { isFallback } = useRouter(); - if (isFallback) return
Loading...
; - - return ( - <> -
- - Go here - - - ); -} - -interface MainProps { - challengeData: Challenge | null; -} - -function Main({ challengeData }: MainProps) { - if (!challengeData || !challengeData?.challengeFiles) return null; - - return ( - <> -
- - - ); -} diff --git a/web/src/page-templates/superblock.tsx b/web/src/page-templates/superblock.tsx deleted file mode 100644 index 7f6d22675cc..00000000000 --- a/web/src/page-templates/superblock.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable jsx-a11y/anchor-is-valid */ -import { InferGetStaticPropsType } from 'next'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; - -// This is circular, but it's only types. -import type { getStaticProps } from '../pages/learn/[superblock]/[blockOrId]'; - -export default function SuperBlock({ - blockNames, - blockNameToChallengeOrderMap, - idToDashedNameMap -}: InferGetStaticPropsType) { - const { isFallback } = useRouter(); - if (isFallback) return
Loading...
; - - return ( - <> - {blockNames.map(blockName => ( -
    - {blockName} -
      - {blockNameToChallengeOrderMap[blockName].map(({ id, title }) => ( -
    • - - {title} - -
    • - ))} -
    -
- ))} - - ); -} diff --git a/web/src/pages/learn/[...id].tsx b/web/src/pages/learn/[...id].tsx deleted file mode 100644 index 11e6d89efee..00000000000 --- a/web/src/pages/learn/[...id].tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { GetStaticPaths, GetStaticProps } from 'next'; -import { - getCurriculum, - getIdToPathSegmentsMap, - PathSegments -} from '../../data-fetching/get-curriculum'; - -// Next expects there to be a React component to render the page. This never -// happens because getStaticProps either redirects to a 404 or to another page, -// but we still need to provide something: -export default function Catch() { - return null; -} - -export const getStaticProps: GetStaticProps = async ({ params }) => { - const idToPathSegmentsMap = getIdToPathSegmentsMap(await getCurriculum()); - const uuid = params?.id?.slice(-1)[0]; - - if (!uuid) { - return { notFound: true, revalidate: 10 }; - } - // TODO: rather than using path segments, use the whole path. This makes this - // more generic and it's easier to redirect to non-challenge pages. i.e. if we - // have the id of a superblock, it won't have three path segments - it will have - // one. - const pathSegments = idToPathSegmentsMap[uuid]; - if (!pathSegments) { - return { notFound: true, revalidate: 10 }; - } - - return { - redirect: { - destination: getDestination(pathSegments), - permanent: false - }, - revalidate: 10 - }; -}; - -export const getDestination = (pathSegments: PathSegments) => { - const { superblock, block, dashedName, id } = pathSegments; - // Currently there are either - if (block && dashedName) { - // challenges: - return `/learn/${superblock}/${block}/${dashedName}/${id}`; - } else { - // or superblocks: - return `/learn/${superblock}/${id}`; - } -}; - -// As with the page component, even though we render 0 pages, this has to exist -export const getStaticPaths: GetStaticPaths = () => ({ - paths: [], - fallback: true -}); diff --git a/web/src/pages/learn/[superblock]/[blockOrId].tsx b/web/src/pages/learn/[superblock]/[blockOrId].tsx deleted file mode 100644 index 93cca51a7e7..00000000000 --- a/web/src/pages/learn/[superblock]/[blockOrId].tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { ParsedUrlQuery } from 'querystring'; -import { GetStaticPaths, GetStaticProps } from 'next'; -import { - getCurriculum, - getIdToDashedNameMap, - getIdToPathSegmentsMap, - PathSegments, - getSuperBlockToBlockMap, - getBlockNameToChallengeOrderMap, - Curriculum -} from '../../../data-fetching/get-curriculum'; -import SuperBlock from '../../../page-templates/superblock'; -import { getDestination } from '../[...id]'; - -interface Props { - blockNames: string[]; - blockNameToChallengeOrderMap: { - [index: string]: { id: string; title: string }; - }; - idToDashedNameMap: { [index: string]: string }; -} - -export default SuperBlock; - -export const getStaticProps: GetStaticProps = async ({ params }) => { - const curriculum = await getCurriculum(); - const idToPathSegmentsMap = getIdToPathSegmentsMap(curriculum); - - // TODO: simplify once noUncheckedIndexedAccess is set. - const pathSegments = idToPathSegmentsMap[params?.blockOrId as string] as - | PathSegments - | undefined; - - if (!pathSegments) return fourOhFour(); - if (pathExists(pathSegments, params)) { - const props = getProps(curriculum); - return renderPage(props); - } else { - return redirect(pathSegments); - } -}; - -const getProps = (curriculum: Curriculum) => { - const idToDashedNameMap = getIdToDashedNameMap(curriculum); - const superBlockToBlockMap = getSuperBlockToBlockMap(curriculum); - - // TODO: figure out how to generate string literal types for these. I think - // the approach has to be to fetch the curriculum and use that to generate a - // type declaration. This won't mean anything in production, but it will be - // helpful when developing. - const blockNames = superBlockToBlockMap['responsive-web-design']; - const blockNameToChallengeOrderMap = getBlockNameToChallengeOrderMap( - curriculum, - blockNames - ); - - return { - blockNames, - blockNameToChallengeOrderMap, - idToDashedNameMap - }; -}; - -const renderPage = (props: Props) => ({ - props, - revalidate: 10 -}); - -const redirect = (pathSegments: PathSegments) => ({ - redirect: { - destination: getDestination(pathSegments), - permanent: false - }, - revalidate: 10 -}); - -// DRY this with [id]'s version -const fourOhFour = () => ({ notFound: true, revalidate: 10 }) as const; - -// DRY this with [id]'s version -const pathExists = (pathSegments: PathSegments, params?: ParsedUrlQuery) => { - const isChallenge = pathSegments.dashedName; - const isExpectedSuperBlockParam = - params?.superblock === pathSegments.superblock; - return !isChallenge && isExpectedSuperBlockParam; -}; - -export const getStaticPaths: GetStaticPaths = () => { - return { - paths: ['/learn/responsive-web-design/special-path'], - fallback: true - }; -}; diff --git a/web/src/pages/learn/[superblock]/[blockOrId]/[dashedName]/[id].tsx b/web/src/pages/learn/[superblock]/[blockOrId]/[dashedName]/[id].tsx deleted file mode 100644 index 19c0fc1aea3..00000000000 --- a/web/src/pages/learn/[superblock]/[blockOrId]/[dashedName]/[id].tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { ParsedUrlQuery } from 'querystring'; -import { GetStaticPaths, GetStaticProps } from 'next'; - -import { - SuperBlock, - Challenge, - getCurriculum, - getIdToPathSegmentsMap, - PathSegments, - getChallengeData, - Curriculum -} from '../../../../../data-fetching/get-curriculum'; -import ChallengeComponent from '../../../../../page-templates/challenge'; -import { getDestination } from '../../../[...id]'; -interface Props { - challengeData: Challenge; -} - -export type { Challenge }; - -export default ChallengeComponent; - -export const getStaticProps: GetStaticProps = async ({ params }) => { - const curriculum = await getCurriculum(); - const idToPathSegmentsMap = getIdToPathSegmentsMap(curriculum); - - // TODO: simplify once noUncheckedIndexedAccess is set. - const pathSegments = idToPathSegmentsMap[params?.id as string] as - | PathSegments - | undefined; - - if (!pathSegments) return fourOhFour(); - if (pathExists(pathSegments, params)) { - const props = getProps(curriculum, pathSegments); - return renderPage(props); - } else { - return redirect(pathSegments); - } -}; - -const getProps = ( - curriculum: Curriculum, - pathSegments: Required -) => ({ - challengeData: getChallengeData(curriculum, pathSegments) -}); -// DRY this with [blockOrId]'s version -const fourOhFour = () => ({ notFound: true, revalidate: 10 }) as const; - -// DRY this with [blockOrId]'s version -const pathExists = ( - pathSegments: PathSegments, - params?: ParsedUrlQuery -): pathSegments is Required => - params?.superblock === pathSegments.superblock && - params?.blockOrId === pathSegments.block && - params?.dashedName === pathSegments.dashedName; - -const renderPage = (props: Props) => ({ - props, - revalidate: 10 -}); - -function redirect(pathSegments: PathSegments) { - return { - redirect: { - destination: getDestination(pathSegments), - permanent: false - }, - revalidate: 10 - }; -} - -export const getStaticPaths: GetStaticPaths = async () => { - const { rwdBlocks } = await getCurriculum(); - - const rwdBlocknames = Object.keys(rwdBlocks); - - // TODO: generalize to all superblocks... OR consider the merits of avoiding - // this entirely. If we skip this the pro is quicker builds and the con is - // that we'd more work onto the webserver. It's probably best to do as much - // work upfront as possible. At least until that upfront work takes too long. - const rwdPaths = rwdBlocknames - .map(name => - rwdBlocks[name].meta.challengeOrder.map(({ id }) => - toParams( - 'responsive-web-design', - name, - getDashedName(rwdBlocks, name, id), - id - ) - ) - ) - .flat(); - - return { - paths: rwdPaths, - fallback: true - }; -}; - -function getDashedName(block: SuperBlock, blockName: string, id: string) { - const challenge = block[blockName].challenges.find(c => c.id === id); - if (!challenge) throw Error(`Challenge ${id} not found in ${blockName}`); - return challenge.dashedName; -} - -function toParams( - superblock: string, - block: string, - dashedName: string, - id: string -) { - return { - params: { - superblock, - blockOrId: block, - dashedName, - id - } - }; -} diff --git a/web/tsconfig.json b/web/tsconfig.json deleted file mode 100644 index edf1ebe902e..00000000000 --- a/web/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "incremental": true, - "module": "esnext", - "isolatedModules": true, - "jsx": "preserve" - // "noUncheckedIndexedAccess": true TODO: add this when you've got time to clean up the code - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"], - "extends": "../tsconfig-base.json" -}