mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-03-29 08:00:43 -04:00
chore: delete web directory and curriculum server (#55071)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
2
.github/workflows/node.js-tests.yml
vendored
2
.github/workflows/node.js-tests.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
1
curriculum-server/.gitignore
vendored
1
curriculum-server/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
data
|
||||
@@ -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 <team@freecodecamp.org>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"extends": "../tsconfig-base.json"
|
||||
}
|
||||
1
web/.gitignore
vendored
1
web/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.next
|
||||
@@ -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.
|
||||
5
web/next-env.d.ts
vendored
5
web/next-env.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -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 <team@freecodecamp.org>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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<PathSegments>) => 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<string, PathSegments> = {};
|
||||
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<PathSegments>
|
||||
) {
|
||||
const superBlockToChallengeMap: SuperBlockToChallengeMap = {
|
||||
'responsive-web-design': (pathSegments: Required<PathSegments>) =>
|
||||
findChallenge(findBlock(rwdBlocks, pathSegments), pathSegments),
|
||||
'javascript-algorithms-and-data-structures': (
|
||||
pathSegments: Required<PathSegments>
|
||||
) => findChallenge(findBlock(jsBlocks, pathSegments), pathSegments)
|
||||
};
|
||||
return superBlockToChallengeMap[pathSegments.superblock](pathSegments);
|
||||
}
|
||||
|
||||
function findBlock(superblock: SuperBlock, params: Required<PathSegments>) {
|
||||
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<string, string> = {};
|
||||
for (const blockName of Object.keys(rwdBlocks)) {
|
||||
const block = rwdBlocks[blockName];
|
||||
for (const challenge of block.challenges) {
|
||||
idToDashedNameMap[challenge.id] = challenge.dashedName;
|
||||
}
|
||||
}
|
||||
return idToDashedNameMap;
|
||||
}
|
||||
@@ -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<typeof getStaticProps>) {
|
||||
const { isFallback } = useRouter();
|
||||
if (isFallback) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Main challengeData={challengeData} />
|
||||
<Link
|
||||
href={
|
||||
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements'
|
||||
}
|
||||
>
|
||||
Go here
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface MainProps {
|
||||
challengeData: Challenge | null;
|
||||
}
|
||||
|
||||
function Main({ challengeData }: MainProps) {
|
||||
if (!challengeData || !challengeData?.challengeFiles) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div dangerouslySetInnerHTML={{ __html: challengeData.description }} />
|
||||
<Editor
|
||||
defaultLanguage={challengeData.challengeFiles[0].ext}
|
||||
height={'50vh'}
|
||||
defaultValue={challengeData.challengeFiles[0].contents}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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<typeof getStaticProps>) {
|
||||
const { isFallback } = useRouter();
|
||||
if (isFallback) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{blockNames.map(blockName => (
|
||||
<ul key={blockName}>
|
||||
{blockName}
|
||||
<ul>
|
||||
{blockNameToChallengeOrderMap[blockName].map(({ id, title }) => (
|
||||
<li key={id}>
|
||||
<Link
|
||||
href={`/learn/responsive-web-design/${blockName}/${idToDashedNameMap[id]}/${id}`}
|
||||
>
|
||||
<a>{title}</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ul>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
@@ -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<Props> = 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
|
||||
};
|
||||
};
|
||||
@@ -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<Props> = 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<PathSegments>
|
||||
) => ({
|
||||
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<PathSegments> =>
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user