chore: import upstream snapshot with attribution
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { ChallengeFile } from '../../src/redux/prop-types';
|
||||
|
||||
export const challengeFiles: ChallengeFile[] = [
|
||||
{
|
||||
contents: 'some ts',
|
||||
error: null,
|
||||
ext: 'ts',
|
||||
history: ['index.ts'],
|
||||
fileKey: 'indexts',
|
||||
name: 'index',
|
||||
seed: 'some ts',
|
||||
editableRegionBoundaries: [],
|
||||
usesMultifileEditor: true,
|
||||
path: 'index.ts'
|
||||
},
|
||||
{
|
||||
contents: 'some css',
|
||||
error: null,
|
||||
ext: 'css',
|
||||
history: ['styles.css'],
|
||||
fileKey: 'stylescss',
|
||||
name: 'styles',
|
||||
seed: 'some css',
|
||||
editableRegionBoundaries: [],
|
||||
usesMultifileEditor: true,
|
||||
path: 'styles.css'
|
||||
},
|
||||
{
|
||||
contents: 'some html',
|
||||
error: null,
|
||||
ext: 'html',
|
||||
history: ['index.html'],
|
||||
fileKey: 'indexhtml',
|
||||
name: 'index',
|
||||
seed: 'some html',
|
||||
editableRegionBoundaries: [],
|
||||
usesMultifileEditor: true,
|
||||
path: 'index.html'
|
||||
},
|
||||
{
|
||||
contents: 'some js',
|
||||
error: null,
|
||||
ext: 'js',
|
||||
history: ['script.js'],
|
||||
fileKey: 'scriptjs',
|
||||
name: 'script',
|
||||
seed: 'some js',
|
||||
editableRegionBoundaries: [],
|
||||
usesMultifileEditor: true,
|
||||
path: 'script.js'
|
||||
},
|
||||
{
|
||||
contents: 'some jsx',
|
||||
error: null,
|
||||
ext: 'jsx',
|
||||
history: ['index.jsx'],
|
||||
fileKey: 'indexjsx',
|
||||
name: 'index',
|
||||
seed: 'some jsx',
|
||||
editableRegionBoundaries: [],
|
||||
usesMultifileEditor: true,
|
||||
path: 'index.jsx'
|
||||
},
|
||||
{
|
||||
contents: 'some tsx',
|
||||
error: null,
|
||||
ext: 'tsx',
|
||||
history: ['index.tsx'],
|
||||
fileKey: 'indextsx',
|
||||
name: 'index',
|
||||
seed: 'some tsx',
|
||||
editableRegionBoundaries: [],
|
||||
usesMultifileEditor: true,
|
||||
path: 'index.tsx'
|
||||
},
|
||||
{
|
||||
contents: '{\n "compilerOptions": {}\n}',
|
||||
error: null,
|
||||
ext: 'json',
|
||||
history: ['tsconfig.json'],
|
||||
fileKey: 'tsconfigjson',
|
||||
name: 'tsconfig',
|
||||
seed: '{\n "compilerOptions": {}\n}',
|
||||
editableRegionBoundaries: [],
|
||||
usesMultifileEditor: true,
|
||||
path: 'tsconfig.json'
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,91 @@
|
||||
const path = require('path');
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const {
|
||||
getBlockCreator,
|
||||
getSuperblocks,
|
||||
superBlockToFilename
|
||||
} = require('@freecodecamp/curriculum/build-curriculum');
|
||||
const {
|
||||
getContentDir,
|
||||
getBlockStructure,
|
||||
getSuperblockStructure,
|
||||
CURRICULUM_DIR
|
||||
} = require('@freecodecamp/curriculum/file-handler');
|
||||
const {
|
||||
transformSuperBlock
|
||||
} = require('@freecodecamp/curriculum/build-superblock');
|
||||
const { getSuperOrder } = require('@freecodecamp/curriculum/super-order');
|
||||
const { readFile } = require('fs/promises');
|
||||
|
||||
const curriculumLocale = process.env.CURRICULUM_LOCALE || 'english';
|
||||
|
||||
exports.localeChallengesRootDir = getContentDir(curriculumLocale);
|
||||
|
||||
const blockCreator = getBlockCreator(curriculumLocale);
|
||||
|
||||
function getBlockMetadata(block, superBlock) {
|
||||
// Compute metadata for the given block in the specified superblock
|
||||
const sbFilename = superBlockToFilename[superBlock];
|
||||
const sbData = getSuperblockStructure(sbFilename);
|
||||
const blocks = transformSuperBlock(sbData, {
|
||||
showComingSoon: process.env.SHOW_UPCOMING_CHANGES === 'true'
|
||||
});
|
||||
|
||||
const order = blocks.findIndex(b => b.dashedName === block);
|
||||
const superOrder = getSuperOrder(superBlock);
|
||||
|
||||
if (order === -1) {
|
||||
throw new Error(`Block ${block} not found in superblock ${superBlock}`);
|
||||
}
|
||||
|
||||
return { order, superOrder };
|
||||
}
|
||||
|
||||
exports.replaceChallengeNodes = () => {
|
||||
return async function replaceChallengeNodes(filePath) {
|
||||
const parentDir = path.dirname(filePath);
|
||||
const block = path.basename(parentDir);
|
||||
const filename = path.basename(filePath);
|
||||
|
||||
const meta = getBlockStructure(block);
|
||||
const superblocks = getSuperblocks(block);
|
||||
|
||||
// Create a challenge for each superblock containing this block
|
||||
const challenges = await Promise.all(
|
||||
superblocks.map(async superBlock => {
|
||||
const { order, superOrder } = getBlockMetadata(block, superBlock);
|
||||
return blockCreator.createChallenge({
|
||||
filename,
|
||||
block,
|
||||
meta: { ...meta, superBlock, order, superOrder },
|
||||
isAudited: true
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return challenges;
|
||||
};
|
||||
};
|
||||
|
||||
exports.buildChallenges = async function buildChallenges() {
|
||||
const curriculum = JSON.parse(
|
||||
await readFile(
|
||||
path.resolve(CURRICULUM_DIR, 'generated', 'curriculum.json'),
|
||||
'utf-8'
|
||||
)
|
||||
);
|
||||
const superBlocks = Object.keys(curriculum);
|
||||
const blocks = superBlocks
|
||||
.map(superBlock => curriculum[superBlock].blocks)
|
||||
.reduce((blocks, superBlock) => {
|
||||
const currentBlocks = Object.keys(superBlock).map(key => superBlock[key]);
|
||||
return blocks.concat(_.flatten(currentBlocks));
|
||||
}, []);
|
||||
|
||||
const builtChallenges = blocks
|
||||
.map(({ challenges }) => challenges)
|
||||
.reduce((accu, current) => accu.concat(current), []);
|
||||
return builtChallenges;
|
||||
};
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
// This is a feature Gatsby adds to the `window` object.
|
||||
// https://github.com/gatsbyjs/gatsby/blob/deb41cdfefbefe0c170b5dd7c10a19ba2b338f6e/packages/gatsby/cache-dir/production-app.js#L28
|
||||
___loader: {
|
||||
enqueue: () => void;
|
||||
hovering: (path: string | null) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,176 @@
|
||||
const path = require('path');
|
||||
const { viewTypes } = require('@freecodecamp/shared/config/challenge-types');
|
||||
|
||||
const backend = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/projects/backend/show.tsx'
|
||||
);
|
||||
const classic = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/classic/show.tsx'
|
||||
);
|
||||
const frontend = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/projects/frontend/show.tsx'
|
||||
);
|
||||
const codeAlly = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/codeally/show.tsx'
|
||||
);
|
||||
const superBlockIntro = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Introduction/super-block-intro.tsx'
|
||||
);
|
||||
const quiz = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/quiz/show.tsx'
|
||||
);
|
||||
|
||||
const exam = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/exam/show.tsx'
|
||||
);
|
||||
|
||||
const msTrophy = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/ms-trophy/show.tsx'
|
||||
);
|
||||
|
||||
const fillInTheBlank = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/fill-in-the-blank/show.tsx'
|
||||
);
|
||||
|
||||
const generic = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/generic/show.tsx'
|
||||
);
|
||||
|
||||
const examDownload = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/exam-download/show.tsx'
|
||||
);
|
||||
|
||||
const views = {
|
||||
backend,
|
||||
classic,
|
||||
modern: classic,
|
||||
frontend,
|
||||
quiz,
|
||||
codeAlly,
|
||||
exam,
|
||||
msTrophy,
|
||||
fillInTheBlank,
|
||||
generic,
|
||||
examDownload
|
||||
};
|
||||
|
||||
function getIsFirstStepInBlock(id, nodes) {
|
||||
const current = nodes[id];
|
||||
const previous = nodes[id - 1];
|
||||
|
||||
if (!previous) return true;
|
||||
return previous.challenge.block !== current.challenge.block;
|
||||
}
|
||||
|
||||
function getTemplateComponent(challengeType) {
|
||||
return views[viewTypes[challengeType]];
|
||||
}
|
||||
|
||||
exports.getTemplateComponent = getTemplateComponent;
|
||||
|
||||
exports.createChallengePages = function (
|
||||
createPage,
|
||||
{ idToNextPathCurrentCurriculum, idToPrevPathCurrentCurriculum }
|
||||
) {
|
||||
return function (node, index, allChallengeNodes) {
|
||||
const {
|
||||
dashedName,
|
||||
disableLoopProtectTests,
|
||||
disableLoopProtectPreview,
|
||||
certification,
|
||||
superBlock,
|
||||
chapter,
|
||||
module,
|
||||
block,
|
||||
fields: { slug, blockHashSlug },
|
||||
required = [],
|
||||
template,
|
||||
challengeType,
|
||||
id,
|
||||
isLastChallengeInBlock,
|
||||
saveSubmissionToDB
|
||||
} = node.challenge;
|
||||
|
||||
createPage({
|
||||
path: slug,
|
||||
component: getTemplateComponent(challengeType),
|
||||
context: {
|
||||
challengeMeta: {
|
||||
blockHashSlug,
|
||||
dashedName,
|
||||
certification,
|
||||
disableLoopProtectTests,
|
||||
disableLoopProtectPreview,
|
||||
superBlock,
|
||||
chapter,
|
||||
module,
|
||||
block,
|
||||
isFirstStep: getIsFirstStepInBlock(index, allChallengeNodes),
|
||||
template,
|
||||
required,
|
||||
isLastChallengeInBlock: isLastChallengeInBlock,
|
||||
nextChallengePath: idToNextPathCurrentCurriculum[node.id],
|
||||
prevChallengePath: idToPrevPathCurrentCurriculum[node.id],
|
||||
id,
|
||||
saveSubmissionToDB
|
||||
},
|
||||
projectPreview: getProjectPreviewConfig(
|
||||
node.challenge,
|
||||
allChallengeNodes
|
||||
),
|
||||
id: node.id
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: figure out a cleaner way to get the last challenge in a block.
|
||||
function getProjectPreviewConfig(challenge, allChallengeNodes) {
|
||||
const { block } = challenge;
|
||||
|
||||
const challengesInBlock = allChallengeNodes
|
||||
.filter(({ challenge }) => challenge.block === block)
|
||||
.map(({ challenge }) => challenge);
|
||||
const lastChallenge = challengesInBlock[challengesInBlock.length - 1];
|
||||
const solutionFiles = lastChallenge.solutions[0] ?? [];
|
||||
const lastChallengeFiles = lastChallenge.challengeFiles ?? [];
|
||||
|
||||
const findFileByKey = (key, files) =>
|
||||
files.find(file => file.fileKey === key);
|
||||
|
||||
const projectPreviewChallengeFiles = lastChallengeFiles.map(file => ({
|
||||
...file,
|
||||
contents:
|
||||
findFileByKey(file.fileKey, solutionFiles)?.contents ?? file.contents
|
||||
}));
|
||||
|
||||
return {
|
||||
challengeData: {
|
||||
challengeType: lastChallenge.challengeType,
|
||||
challengeFiles: projectPreviewChallengeFiles
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
exports.createSuperBlockIntroPages = function (createPage) {
|
||||
return function ({ superBlock }) {
|
||||
createPage({
|
||||
path: `/learn/${superBlock}/`,
|
||||
component: superBlockIntro,
|
||||
context: {
|
||||
superBlock
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('./challenge-page-creator');
|
||||
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import ShallowRenderer from 'react-test-renderer/shallow';
|
||||
import { describe, test, expect, vi } from 'vitest';
|
||||
|
||||
import FourOhFourPage from '../../src/pages/404';
|
||||
import Certification from '../../src/pages/certification';
|
||||
import Learn from '../../src/pages/learn';
|
||||
import { createStore } from '../../src/redux/create-store';
|
||||
import layoutSelector from './layout-selector';
|
||||
|
||||
vi.mock('../../src/analytics');
|
||||
|
||||
vi.mock('../../src/utils/get-words');
|
||||
|
||||
const store = createStore();
|
||||
|
||||
// TODO: rather than testing which props passed from layoutSelector to the
|
||||
// component it renders, test that the rendered component has the expected
|
||||
// features (i.e. has a footer or not, etc.). That should be possible in
|
||||
// react-testing-library.
|
||||
|
||||
interface NameAndProps {
|
||||
props: Record<string, unknown>;
|
||||
name: string;
|
||||
}
|
||||
function getComponentNameAndProps(
|
||||
elementType: React.JSXElementConstructor<never>,
|
||||
pathname: string,
|
||||
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } }
|
||||
): NameAndProps {
|
||||
const utils = ShallowRenderer.createRenderer();
|
||||
const LayoutReactComponent = layoutSelector({
|
||||
element: { type: elementType, props: {}, key: '' },
|
||||
props: {
|
||||
data: {},
|
||||
location: {
|
||||
pathname
|
||||
},
|
||||
pageContext,
|
||||
params: { '*': '' },
|
||||
path: ''
|
||||
}
|
||||
});
|
||||
utils.render(<Provider store={store}>{LayoutReactComponent}</Provider>);
|
||||
const view = utils.getRenderOutput();
|
||||
return {
|
||||
props: view.props.children.props as Record<string, unknown>,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
name: view.props.children.type.WrappedComponent.displayName
|
||||
// TODO: Revisit this when react-test-renderer is replaced with
|
||||
// react-testing-library
|
||||
};
|
||||
}
|
||||
|
||||
const challengePageContext = {
|
||||
challengeMeta: {
|
||||
block: 'Basic HTML and HTML5',
|
||||
superBlock: 'responsive-web-design'
|
||||
}
|
||||
};
|
||||
|
||||
describe('Layout selector', () => {
|
||||
test('Challenges should have DefaultLayout and no footer', () => {
|
||||
const challengePath =
|
||||
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements';
|
||||
const componentObj = getComponentNameAndProps(
|
||||
Learn,
|
||||
challengePath,
|
||||
challengePageContext
|
||||
);
|
||||
expect(componentObj.name).toEqual('DefaultLayout');
|
||||
expect(componentObj.props.showFooter).toEqual(false);
|
||||
});
|
||||
|
||||
test('SuperBlock path should have DefaultLayout and footer', () => {
|
||||
const superBlockPath = '/learn/responsive-web-design/';
|
||||
const componentObj = getComponentNameAndProps(Learn, superBlockPath);
|
||||
expect(componentObj.name).toEqual('DefaultLayout');
|
||||
expect(componentObj.props.showFooter).toEqual(true);
|
||||
});
|
||||
|
||||
test('i18n challenge path should have DefaultLayout and no footer', () => {
|
||||
const challengePath =
|
||||
'espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/';
|
||||
const componentObj = getComponentNameAndProps(
|
||||
Learn,
|
||||
challengePath,
|
||||
challengePageContext
|
||||
);
|
||||
expect(componentObj.name).toEqual('DefaultLayout');
|
||||
expect(componentObj.props.showFooter).toEqual(false);
|
||||
});
|
||||
|
||||
test('i18n superBlock path should have DefaultLayout and footer', () => {
|
||||
const superBlockPath = '/learn/responsive-web-design/';
|
||||
const componentObj = getComponentNameAndProps(Learn, superBlockPath);
|
||||
expect(componentObj.name).toEqual('DefaultLayout');
|
||||
expect(componentObj.props.showFooter).toEqual(true);
|
||||
});
|
||||
|
||||
test('404 page should have DefaultLayout and footer', () => {
|
||||
const challengePath =
|
||||
'/espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/';
|
||||
const componentObj = getComponentNameAndProps(
|
||||
FourOhFourPage,
|
||||
challengePath
|
||||
);
|
||||
expect(componentObj.name).toEqual('DefaultLayout');
|
||||
expect(componentObj.props.showFooter).toEqual(true);
|
||||
});
|
||||
|
||||
test('Certification path should have CertificationLayout', () => {
|
||||
const challengePath =
|
||||
'/certification/mot01/javascript-algorithms-and-data-structures/';
|
||||
const componentObj = getComponentNameAndProps(Certification, challengePath);
|
||||
expect(componentObj.name).toEqual('CertificationLayout');
|
||||
});
|
||||
|
||||
test('Status paths should return raw element without layout', () => {
|
||||
const TestComponent = () => <div>Test</div>;
|
||||
const statusPath = '/status/version';
|
||||
|
||||
const result = layoutSelector({
|
||||
element: { type: TestComponent, props: {}, key: '' },
|
||||
props: {
|
||||
data: {},
|
||||
location: {
|
||||
pathname: statusPath
|
||||
},
|
||||
params: { '*': '' },
|
||||
path: ''
|
||||
}
|
||||
});
|
||||
|
||||
// The result should be the element directly, not wrapped in a layout
|
||||
expect(result.type).toBe(TestComponent);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
|
||||
import CertificationLayout from '../../src/components/layouts/certification';
|
||||
import DefaultLayout from '../../src/components/layouts/default';
|
||||
import FourOhFourPage from '../../src/pages/404';
|
||||
|
||||
interface LayoutSelectorProps {
|
||||
element: JSX.Element;
|
||||
props: {
|
||||
data: { challengeNode?: { challenge?: { usesMultifileEditor?: boolean } } };
|
||||
location: { pathname: string };
|
||||
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } };
|
||||
params: { '*'?: string };
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
export default function layoutSelector({
|
||||
element,
|
||||
props
|
||||
}: LayoutSelectorProps): JSX.Element {
|
||||
const {
|
||||
location: { pathname }
|
||||
} = props;
|
||||
|
||||
const isDailyChallenge = props.path === '/learn/daily-coding-challenge/*';
|
||||
const dailyChallengeParam = props.params['*'];
|
||||
|
||||
const isChallenge = !!props.pageContext?.challengeMeta || isDailyChallenge;
|
||||
|
||||
// Return raw element for status endpoints without any layout
|
||||
if (/^\/status\//.test(pathname)) {
|
||||
return element;
|
||||
}
|
||||
|
||||
if (element.type === FourOhFourPage) {
|
||||
return (
|
||||
<DefaultLayout pathname={pathname} showFooter={true}>
|
||||
{element}
|
||||
</DefaultLayout>
|
||||
);
|
||||
} else if (/\/certification\//.test(pathname)) {
|
||||
return (
|
||||
<CertificationLayout pathname={pathname}>{element}</CertificationLayout>
|
||||
);
|
||||
} else if (isChallenge) {
|
||||
return (
|
||||
<DefaultLayout
|
||||
pathname={pathname}
|
||||
showFooter={false}
|
||||
isChallenge={true}
|
||||
isDailyChallenge={isDailyChallenge}
|
||||
dailyChallengeParam={dailyChallengeParam}
|
||||
usesMultifileEditor={
|
||||
props.data?.challengeNode?.challenge?.usesMultifileEditor
|
||||
}
|
||||
block={props.pageContext?.challengeMeta?.block}
|
||||
superBlock={props.pageContext?.challengeMeta?.superBlock}
|
||||
>
|
||||
{element}
|
||||
</DefaultLayout>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DefaultLayout pathname={pathname} showFooter={true}>
|
||||
{element}
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* The function is useful in cases where we want to preload a page
|
||||
* but the link of the page isn't rendered on the screen.
|
||||
* For more details, see https://github.com/freeCodeCamp/freeCodeCamp/pull/55472.
|
||||
*/
|
||||
export const preloadPage = (path?: string) => {
|
||||
if (!window.___loader || !path) return;
|
||||
|
||||
window.___loader.hovering(path);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
const envData = require('../../config/env.json');
|
||||
|
||||
const { clientLocale } = envData;
|
||||
|
||||
exports.pathPrefix = clientLocale === 'english' ? '' : '/' + clientLocale;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Certification } from '@freecodecamp/shared/config/certification-settings';
|
||||
|
||||
const idToPath = new Map(
|
||||
Object.entries({
|
||||
'561add10cb82ac38a17523bc': Certification.BackEndDevApis,
|
||||
'5a553ca864b52e1d8bceea14': Certification.DataVis,
|
||||
'561acd10cb82ac38a17513bc': Certification.FrontEndDevLibs,
|
||||
'5e611829481575a52dc59c0e': Certification.QualityAssurance,
|
||||
'5e6021435ac9d0ecd8b94b00': Certification.InfoSec,
|
||||
'561abd10cb81ac38a17513bc': Certification.JsAlgoDataStruct,
|
||||
'561add10cb82ac38a17513bc': Certification.RespWebDesign,
|
||||
'660add10cb82ac38a17513be': Certification.LegacyBackEnd,
|
||||
'561add10cb82ac39a17513bc': Certification.LegacyDataVis,
|
||||
'561add10cb82ac38a17513be': Certification.LegacyFrontEnd,
|
||||
'561add10cb82ac38a17213bc': Certification.LegacyInfoSecQa,
|
||||
'561add10cb82ac38a17213bd': Certification.LegacyFullStack,
|
||||
'5e44431b903586ffb414c951': Certification.SciCompPy,
|
||||
'5e46fc95ac417301a38fb934': Certification.DataAnalysisPy,
|
||||
'5e46fc95ac417301a38fb935': Certification.MachineLearningPy,
|
||||
'68db314d3c11a8bff07c7535': Certification.RespWebDesignV9,
|
||||
'68c4069c1ef859270e17c495': Certification.JsV9,
|
||||
'68e008aa5f80c6099d47b3a2': Certification.FrontEndDevLibsV9,
|
||||
'68e6bd5020effa1586e79855': Certification.PythonV9,
|
||||
'68e6bd5120effa1586e79856': Certification.RelationalDbV9,
|
||||
'68e6bd5120effa1586e79857': Certification.BackEndDevApisV9,
|
||||
'64514fda6c245de4d11eb7bb': Certification.FullStackDeveloperV9,
|
||||
'651dd7e01d697d0aab7833b7': Certification.A2English
|
||||
})
|
||||
);
|
||||
|
||||
export const getCertIds = (): IterableIterator<string> => idToPath.keys();
|
||||
export const getPathFromID = (id: string): string | undefined =>
|
||||
idToPath.get(id);
|
||||
|
||||
export function isBrowser(): boolean {
|
||||
return typeof window !== 'undefined';
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { challengeFiles } from './__fixtures__/challenges';
|
||||
import { sortChallengeFiles } from './sort-challengefiles';
|
||||
|
||||
describe('sort-files', () => {
|
||||
describe('sortChallengeFiles', () => {
|
||||
it('should return an array', () => {
|
||||
const sorted = sortChallengeFiles(challengeFiles);
|
||||
expect(Array.isArray(sorted)).toBe(true);
|
||||
});
|
||||
it('should not modify the challenges', () => {
|
||||
const sorted = sortChallengeFiles(challengeFiles);
|
||||
const expected = challengeFiles;
|
||||
expect(sorted).toEqual(expect.arrayContaining(expected));
|
||||
expect(sorted.length).toEqual(expected.length);
|
||||
});
|
||||
|
||||
it('should sort the objects into jsx, tsx, html, css, js, ts, tsconfig order', () => {
|
||||
const sorted = sortChallengeFiles(challengeFiles);
|
||||
const sortedKeys = sorted.map(({ fileKey }) => fileKey);
|
||||
const expected = [
|
||||
'indexjsx',
|
||||
'indextsx',
|
||||
'indexhtml',
|
||||
'stylescss',
|
||||
'scriptjs',
|
||||
'indexts',
|
||||
'tsconfigjson'
|
||||
];
|
||||
expect(sortedKeys).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
export function sortChallengeFiles<File extends { fileKey: string }>(
|
||||
challengeFiles: File[]
|
||||
): File[] {
|
||||
return challengeFiles.toSorted((a, b) => {
|
||||
if (a.fileKey === 'indexjsx') return -1;
|
||||
if (b.fileKey === 'indexjsx') return 1;
|
||||
if (a.fileKey === 'indextsx') return -1;
|
||||
if (b.fileKey === 'indextsx') return 1;
|
||||
if (a.fileKey === 'indexhtml') return -1;
|
||||
if (b.fileKey === 'indexhtml') return 1;
|
||||
if (a.fileKey === 'stylescss') return -1;
|
||||
if (b.fileKey === 'stylescss') return 1;
|
||||
if (a.fileKey === 'scriptjs') return -1;
|
||||
if (b.fileKey === 'scriptjs') return 1;
|
||||
if (a.fileKey === 'indexts') return -1;
|
||||
if (b.fileKey === 'indexts') return 1;
|
||||
if (a.fileKey === 'tsconfigjson') return -1;
|
||||
if (b.fileKey === 'tsconfigjson') return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import i18next from 'i18next';
|
||||
import React from 'react';
|
||||
|
||||
import { isMathJaxAllowed, mathJaxSrc } from '../src/utils/math-jax';
|
||||
|
||||
export function getheadTagComponents(): JSX.Element[] {
|
||||
const socialImage =
|
||||
'https://cdn.freecodecamp.org/platform/universal/fcc_meta_1920X1080-indigo.png';
|
||||
|
||||
const headTags = [
|
||||
<meta content='freeCodeCamp.org' key='og:title' name='og:title' />,
|
||||
<meta
|
||||
content={i18next.t('metaTags:social-description')}
|
||||
key='og:description'
|
||||
name='og:description'
|
||||
/>,
|
||||
<meta content={socialImage} key='og:image' property='og:image' />,
|
||||
<meta
|
||||
content='summary_large_image'
|
||||
key='twitter:card'
|
||||
name='twitter:card'
|
||||
/>,
|
||||
<meta
|
||||
content={socialImage}
|
||||
key='twitter:image:src'
|
||||
name='twitter:image:src'
|
||||
/>,
|
||||
<meta
|
||||
content='freeCodeCamp.org'
|
||||
key='twitter:title'
|
||||
name='twitter:title'
|
||||
/>,
|
||||
<meta
|
||||
content={i18next.t('metaTags:social-description')}
|
||||
key='twitter:description'
|
||||
name='twitter:description'
|
||||
/>,
|
||||
<meta
|
||||
content='$ilp.uphold.com/LJmbPn7WD4JB'
|
||||
key='monetization'
|
||||
name='monetization'
|
||||
/>
|
||||
];
|
||||
return headTags;
|
||||
}
|
||||
|
||||
export function getPostBodyComponents(superblock: string): JSX.Element[] {
|
||||
const scripts = [];
|
||||
const mathJaxScriptElement = (
|
||||
<script
|
||||
async={false}
|
||||
id='mathjax'
|
||||
key='mathjax'
|
||||
src={mathJaxSrc}
|
||||
type='text/javascript'
|
||||
/>
|
||||
);
|
||||
|
||||
if (isMathJaxAllowed(superblock)) {
|
||||
scripts.push(mathJaxScriptElement);
|
||||
}
|
||||
|
||||
return scripts.filter(Boolean);
|
||||
}
|
||||
|
||||
export function getPreBodyThemeScript(): JSX.Element[] {
|
||||
const script = (
|
||||
<script
|
||||
key='prebody-theme-init'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(){
|
||||
let theme = 'light';
|
||||
const localTheme = localStorage.getItem('theme');
|
||||
|
||||
if (localTheme !== null) {
|
||||
theme = localTheme === 'dark' ? 'dark' : 'light';
|
||||
} else if (
|
||||
window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
) {
|
||||
theme = 'dark';
|
||||
}
|
||||
|
||||
const bodyEl = document && document.body;
|
||||
|
||||
if (bodyEl && bodyEl.classList) {
|
||||
bodyEl.classList.remove('light-palette');
|
||||
bodyEl.classList.remove('dark-palette');
|
||||
bodyEl.classList.add(theme + '-palette');
|
||||
}
|
||||
})();`
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return [script];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React, { type ReactElement, type ReactNode } from 'react';
|
||||
import { render as rtlRender } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import type { Store } from 'redux';
|
||||
|
||||
function render(ui: ReactElement, store: Store) {
|
||||
function Wrapper({ children }: { children: ReactNode }) {
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
}
|
||||
return rtlRender(ui, { wrapper: Wrapper });
|
||||
}
|
||||
|
||||
// re-export everything
|
||||
// eslint-disable-next-line import/export
|
||||
export * from '@testing-library/react';
|
||||
// override render method
|
||||
// eslint-disable-next-line import/export
|
||||
export { render };
|
||||
Reference in New Issue
Block a user