mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client/curriculum): add generic challenge and first review block (#56631)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -1727,7 +1727,10 @@
|
|||||||
"intro": ["For this lab, you will create a video compilation web page."]
|
"intro": ["For this lab, you will create a video compilation web page."]
|
||||||
},
|
},
|
||||||
"bzfv": { "title": "8", "intro": [] },
|
"bzfv": { "title": "8", "intro": [] },
|
||||||
"snuv": { "title": "9", "intro": [] },
|
"review-basic-html": {
|
||||||
|
"title": "Basic HTML Review",
|
||||||
|
"intro": ["Review the basic HTML topics."]
|
||||||
|
},
|
||||||
"quiz-basic-html": {
|
"quiz-basic-html": {
|
||||||
"title": "Basic HTML Quiz",
|
"title": "Basic HTML Quiz",
|
||||||
"intro": [
|
"intro": [
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: Introduction to the Basic HTML Review
|
||||||
|
block: review-basic-html
|
||||||
|
superBlock: front-end-development
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction to the Basic HTML Review
|
||||||
|
|
||||||
|
Review the basic HTML topics.
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import { graphql } from 'gatsby';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import Helmet from 'react-helmet';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Container, Col, Row, Button } from '@freecodecamp/ui';
|
||||||
|
|
||||||
|
// Local Utilities
|
||||||
|
import Spacer from '../../../components/helpers/spacer';
|
||||||
|
import LearnLayout from '../../../components/layouts/learn';
|
||||||
|
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types';
|
||||||
|
import ChallengeDescription from '../components/challenge-description';
|
||||||
|
import Hotkeys from '../components/hotkeys';
|
||||||
|
import ChallengeTitle from '../components/challenge-title';
|
||||||
|
import VideoPlayer from '../components/video-player';
|
||||||
|
import CompletionModal from '../components/completion-modal';
|
||||||
|
import Assignments from '../components/assignments';
|
||||||
|
import {
|
||||||
|
challengeMounted,
|
||||||
|
updateChallengeMeta,
|
||||||
|
openModal,
|
||||||
|
updateSolutionFormValues,
|
||||||
|
initTests
|
||||||
|
} from '../redux/actions';
|
||||||
|
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||||
|
import { BlockTypes } from '../../../../../shared/config/blocks';
|
||||||
|
|
||||||
|
// Redux Setup
|
||||||
|
const mapStateToProps = (state: unknown) => ({
|
||||||
|
isChallengeCompleted: isChallengeCompletedSelector(state) as boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
initTests,
|
||||||
|
updateChallengeMeta,
|
||||||
|
challengeMounted,
|
||||||
|
updateSolutionFormValues,
|
||||||
|
openCompletionModal: () => openModal('completion')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Types
|
||||||
|
interface ShowQuizProps {
|
||||||
|
challengeMounted: (arg0: string) => void;
|
||||||
|
data: { challengeNode: ChallengeNode };
|
||||||
|
description: string;
|
||||||
|
initTests: (xs: Test[]) => void;
|
||||||
|
isChallengeCompleted: boolean;
|
||||||
|
openCompletionModal: () => void;
|
||||||
|
pageContext: {
|
||||||
|
challengeMeta: ChallengeMeta;
|
||||||
|
};
|
||||||
|
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
||||||
|
updateSolutionFormValues: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShowGeneric = ({
|
||||||
|
challengeMounted,
|
||||||
|
data: {
|
||||||
|
challengeNode: {
|
||||||
|
challenge: {
|
||||||
|
assignments,
|
||||||
|
bilibiliIds,
|
||||||
|
block,
|
||||||
|
blockType,
|
||||||
|
description,
|
||||||
|
challengeType,
|
||||||
|
fields: { tests },
|
||||||
|
helpCategory,
|
||||||
|
instructions,
|
||||||
|
title,
|
||||||
|
translationPending,
|
||||||
|
superBlock,
|
||||||
|
videoId,
|
||||||
|
videoLocaleIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageContext: { challengeMeta },
|
||||||
|
initTests,
|
||||||
|
updateChallengeMeta,
|
||||||
|
openCompletionModal,
|
||||||
|
isChallengeCompleted
|
||||||
|
}: ShowQuizProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { nextChallengePath, prevChallengePath } = challengeMeta;
|
||||||
|
const container = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const blockNameTitle = `${t(
|
||||||
|
`intro:${superBlock}.blocks.${block}.title`
|
||||||
|
)} - ${title}`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initTests(tests);
|
||||||
|
updateChallengeMeta({
|
||||||
|
...challengeMeta,
|
||||||
|
title,
|
||||||
|
challengeType,
|
||||||
|
helpCategory
|
||||||
|
});
|
||||||
|
challengeMounted(challengeMeta.id);
|
||||||
|
container.current?.focus();
|
||||||
|
// This effect should be run once on mount
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateChallengeMeta({
|
||||||
|
...challengeMeta,
|
||||||
|
title,
|
||||||
|
challengeType,
|
||||||
|
helpCategory
|
||||||
|
});
|
||||||
|
challengeMounted(challengeMeta.id);
|
||||||
|
}, [
|
||||||
|
title,
|
||||||
|
challengeMeta,
|
||||||
|
challengeType,
|
||||||
|
helpCategory,
|
||||||
|
challengeMounted,
|
||||||
|
updateChallengeMeta
|
||||||
|
]);
|
||||||
|
|
||||||
|
// video
|
||||||
|
const [videoIsLoaded, setVideoIsLoaded] = useState(false);
|
||||||
|
|
||||||
|
const handleVideoIsLoaded = () => {
|
||||||
|
setVideoIsLoaded(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// assignments
|
||||||
|
const [assignmentsCompleted, setAssignmentsCompleted] = useState(0);
|
||||||
|
const allAssignmentsCompleted = assignmentsCompleted === assignments.length;
|
||||||
|
|
||||||
|
const handleAssignmentChange = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const isCompleted = event.target.checked; // extract value before target is nullified
|
||||||
|
setAssignmentsCompleted(a => (isCompleted ? a + 1 : a - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
// submit
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (assignments.length == 0 || allAssignmentsCompleted) {
|
||||||
|
openCompletionModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Hotkeys
|
||||||
|
executeChallenge={handleSubmit}
|
||||||
|
containerRef={container}
|
||||||
|
nextChallengePath={nextChallengePath}
|
||||||
|
prevChallengePath={prevChallengePath}
|
||||||
|
>
|
||||||
|
<LearnLayout>
|
||||||
|
<Helmet
|
||||||
|
title={`${blockNameTitle} | ${t('learn.learn')} | freeCodeCamp.org`}
|
||||||
|
/>
|
||||||
|
<Container>
|
||||||
|
<Row>
|
||||||
|
<Spacer size='medium' />
|
||||||
|
<ChallengeTitle
|
||||||
|
isCompleted={isChallengeCompleted}
|
||||||
|
translationPending={translationPending}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</ChallengeTitle>
|
||||||
|
|
||||||
|
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
|
{description && (
|
||||||
|
<ChallengeDescription description={description} />
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col lg={10} lgOffset={1} md={10} mdOffset={1}>
|
||||||
|
{videoId && (
|
||||||
|
<VideoPlayer
|
||||||
|
bilibiliIds={bilibiliIds}
|
||||||
|
onVideoLoad={handleVideoIsLoaded}
|
||||||
|
title={title}
|
||||||
|
videoId={videoId}
|
||||||
|
videoIsLoaded={videoIsLoaded}
|
||||||
|
videoLocaleIds={videoLocaleIds}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
|
{instructions && (
|
||||||
|
<ChallengeDescription description={instructions} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Spacer size='medium' />
|
||||||
|
|
||||||
|
{assignments.length > 0 && (
|
||||||
|
<Assignments
|
||||||
|
assignments={assignments}
|
||||||
|
allAssignmentsCompleted={allAssignmentsCompleted}
|
||||||
|
handleAssignmentChange={handleAssignmentChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button block={true} variant='primary' onClick={handleSubmit}>
|
||||||
|
{blockType === BlockTypes.review
|
||||||
|
? t('buttons.submit')
|
||||||
|
: t('buttons.check-answer')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Spacer size='large' />
|
||||||
|
</Col>
|
||||||
|
<CompletionModal />
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</LearnLayout>
|
||||||
|
</Hotkeys>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ShowGeneric.displayName = 'ShowGeneric';
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ShowGeneric);
|
||||||
|
|
||||||
|
export const query = graphql`
|
||||||
|
query GenericChallenge($id: String!) {
|
||||||
|
challengeNode(id: { eq: $id }) {
|
||||||
|
challenge {
|
||||||
|
assignments
|
||||||
|
bilibiliIds {
|
||||||
|
aid
|
||||||
|
bvid
|
||||||
|
cid
|
||||||
|
}
|
||||||
|
block
|
||||||
|
blockType
|
||||||
|
challengeType
|
||||||
|
description
|
||||||
|
helpCategory
|
||||||
|
instructions
|
||||||
|
fields {
|
||||||
|
blockName
|
||||||
|
slug
|
||||||
|
tests {
|
||||||
|
text
|
||||||
|
testString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
superBlock
|
||||||
|
title
|
||||||
|
translationPending
|
||||||
|
videoId
|
||||||
|
videoId
|
||||||
|
videoLocaleIds {
|
||||||
|
espanol
|
||||||
|
italian
|
||||||
|
portuguese
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -61,6 +61,11 @@ const fillInTheBlank = path.resolve(
|
|||||||
'../../src/templates/Challenges/fill-in-the-blank/show.tsx'
|
'../../src/templates/Challenges/fill-in-the-blank/show.tsx'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const generic = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../src/templates/Challenges/generic/show.tsx'
|
||||||
|
);
|
||||||
|
|
||||||
const views = {
|
const views = {
|
||||||
backend,
|
backend,
|
||||||
classic,
|
classic,
|
||||||
@@ -73,8 +78,8 @@ const views = {
|
|||||||
exam,
|
exam,
|
||||||
msTrophy,
|
msTrophy,
|
||||||
dialogue,
|
dialogue,
|
||||||
fillInTheBlank
|
fillInTheBlank,
|
||||||
// quiz: Quiz
|
generic
|
||||||
};
|
};
|
||||||
|
|
||||||
function getIsFirstStepInBlock(id, edges) {
|
function getIsFirstStepInBlock(id, edges) {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "Basic HTML Review",
|
||||||
|
"isUpcomingChange": true,
|
||||||
|
"dashedName": "review-basic-html",
|
||||||
|
"order": 9,
|
||||||
|
"superBlock": "front-end-development",
|
||||||
|
"challengeOrder": [{ "id": "67072fc183c7ca6c588feb4d", "title": "Basic HTML Review" }],
|
||||||
|
"helpCategory": "HTML-CSS",
|
||||||
|
"blockType": "review"
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
id: 67072fc183c7ca6c588feb4d
|
||||||
|
title: Basic HTML Review
|
||||||
|
challengeType: 24
|
||||||
|
dashedName: basic-html-review
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
Review the concepts below to prepare for the upcoming quiz.
|
||||||
|
|
||||||
|
## HTML Basics
|
||||||
|
|
||||||
|
- **Role of HTML**: Foundation of web structure.
|
||||||
|
- **Block-level vs. inline elements**: Understanding `div` and `span`.
|
||||||
|
- **Attributes**: Adding metadata and behavior to elements.
|
||||||
|
|
||||||
|
## Identifiers and Grouping
|
||||||
|
|
||||||
|
- **IDs**: Unique element identifiers.
|
||||||
|
- **Classes**: Grouping elements for styling and behavior.
|
||||||
|
|
||||||
|
## Special Characters and Linking
|
||||||
|
|
||||||
|
- **HTML entities**: Using special characters like `&` and `<`.
|
||||||
|
- **`<link>` element**: Linking to external stylesheets.
|
||||||
|
- **`<script>` element**: Embedding external JavaScript files.
|
||||||
|
|
||||||
|
## Boilerplate and Encoding
|
||||||
|
|
||||||
|
- **HTML boilerplate**: Basic structure of a webpage.
|
||||||
|
- **UTF-8 character encoding**: Ensuring universal character display.
|
||||||
|
|
||||||
|
## SEO and Social Sharing
|
||||||
|
|
||||||
|
- **Meta tags (`description`)**: How it impacts SEO.
|
||||||
|
- **Open Graph tags**: Enhancing social media sharing.
|
||||||
|
|
||||||
|
## Media Elements and Optimization
|
||||||
|
|
||||||
|
- **Replaced elements**: Embedded content (e.g., images, iframes).
|
||||||
|
- **Optimizing media**: Techniques to improve media performance.
|
||||||
|
- **Image formats and licenses**: Understanding usage rights and types.
|
||||||
|
- **SVGs**: Scalable vector graphics for sharp visuals.
|
||||||
|
|
||||||
|
## Multimedia Integration
|
||||||
|
|
||||||
|
- **HTML audio and video elements**: Embedding multimedia.
|
||||||
|
- **Embedding with `<iframe>`**: Integrating external video content.
|
||||||
|
|
||||||
|
## Paths and Link Behavior
|
||||||
|
|
||||||
|
- **Target attribute types**: Controlling link behavior.
|
||||||
|
- **Absolute vs. relative paths**: Navigating directories.
|
||||||
|
- **Path syntax**: Understanding `/`, `./`, `../` for file navigation.
|
||||||
|
- **Link states**: Managing different link interactions (hover, active).
|
||||||
|
- **Inline vs. block-level links**: Layout and behavior differences.
|
||||||
|
|
||||||
|
# --assignment--
|
||||||
|
|
||||||
|
Review the Basic HTML topics and concepts.
|
||||||
@@ -143,7 +143,7 @@ const schema = Joi.object()
|
|||||||
}),
|
}),
|
||||||
challengeOrder: Joi.number(),
|
challengeOrder: Joi.number(),
|
||||||
certification: Joi.string().regex(slugWithSlashRE),
|
certification: Joi.string().regex(slugWithSlashRE),
|
||||||
challengeType: Joi.number().min(0).max(23).required(),
|
challengeType: Joi.number().min(0).max(24).required(),
|
||||||
checksum: Joi.number(),
|
checksum: Joi.number(),
|
||||||
// TODO: require this only for normal challenges, not certs
|
// TODO: require this only for normal challenges, not certs
|
||||||
dashedName: Joi.string().regex(slugRE),
|
dashedName: Joi.string().regex(slugRE),
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ const schema = Joi.object()
|
|||||||
}),
|
}),
|
||||||
challengeOrder: Joi.number(),
|
challengeOrder: Joi.number(),
|
||||||
certification: Joi.string().regex(slugWithSlashRE),
|
certification: Joi.string().regex(slugWithSlashRE),
|
||||||
challengeType: Joi.number().min(0).max(23).required(),
|
challengeType: Joi.number().min(0).max(24).required(),
|
||||||
checksum: Joi.number(),
|
checksum: Joi.number(),
|
||||||
// TODO: require this only for normal challenges, not certs
|
// TODO: require this only for normal challenges, not certs
|
||||||
dashedName: Joi.string().regex(slugRE),
|
dashedName: Joi.string().regex(slugRE),
|
||||||
|
|||||||
@@ -6,13 +6,20 @@ const slugWithSlashRE = new RegExp('^[a-z0-9-/]+$');
|
|||||||
const schema = Joi.object()
|
const schema = Joi.object()
|
||||||
.keys({
|
.keys({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
blockType: Joi.valid('workshop', 'lab', 'lecture', 'quiz', 'exam'),
|
|
||||||
blockLayout: Joi.valid(
|
blockLayout: Joi.valid(
|
||||||
'challenge-list',
|
'challenge-list',
|
||||||
'challenge-grid',
|
'challenge-grid',
|
||||||
'link',
|
'link',
|
||||||
'project-list'
|
'project-list'
|
||||||
),
|
),
|
||||||
|
blockType: Joi.valid(
|
||||||
|
'workshop',
|
||||||
|
'lab',
|
||||||
|
'lecture',
|
||||||
|
'review',
|
||||||
|
'quiz',
|
||||||
|
'exam'
|
||||||
|
),
|
||||||
isUpcomingChange: Joi.boolean().required(),
|
isUpcomingChange: Joi.boolean().required(),
|
||||||
dashedName: Joi.string().regex(slugRE).required(),
|
dashedName: Joi.string().regex(slugRE).required(),
|
||||||
superBlock: Joi.string().regex(slugWithSlashRE).required(),
|
superBlock: Joi.string().regex(slugWithSlashRE).required(),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const python = 20;
|
|||||||
const dialogue = 21;
|
const dialogue = 21;
|
||||||
const fillInTheBlank = 22;
|
const fillInTheBlank = 22;
|
||||||
const multifilePythonCertProject = 23;
|
const multifilePythonCertProject = 23;
|
||||||
|
const generic = 24;
|
||||||
|
|
||||||
export const challengeTypes = {
|
export const challengeTypes = {
|
||||||
html,
|
html,
|
||||||
@@ -49,7 +50,8 @@ export const challengeTypes = {
|
|||||||
python,
|
python,
|
||||||
dialogue,
|
dialogue,
|
||||||
fillInTheBlank,
|
fillInTheBlank,
|
||||||
multifilePythonCertProject
|
multifilePythonCertProject,
|
||||||
|
generic
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasNoSolution = (challengeType: number): boolean => {
|
export const hasNoSolution = (challengeType: number): boolean => {
|
||||||
@@ -71,7 +73,8 @@ export const hasNoSolution = (challengeType: number): boolean => {
|
|||||||
msTrophy,
|
msTrophy,
|
||||||
multipleChoice,
|
multipleChoice,
|
||||||
dialogue,
|
dialogue,
|
||||||
fillInTheBlank
|
fillInTheBlank,
|
||||||
|
generic
|
||||||
];
|
];
|
||||||
|
|
||||||
return noSolutions.includes(challengeType);
|
return noSolutions.includes(challengeType);
|
||||||
@@ -101,7 +104,8 @@ export const viewTypes = {
|
|||||||
[python]: 'modern',
|
[python]: 'modern',
|
||||||
[dialogue]: 'dialogue',
|
[dialogue]: 'dialogue',
|
||||||
[fillInTheBlank]: 'fillInTheBlank',
|
[fillInTheBlank]: 'fillInTheBlank',
|
||||||
[multifilePythonCertProject]: 'classic'
|
[multifilePythonCertProject]: 'classic',
|
||||||
|
[generic]: 'generic'
|
||||||
};
|
};
|
||||||
|
|
||||||
// determine the type of submit function to use for the challenge on completion
|
// determine the type of submit function to use for the challenge on completion
|
||||||
@@ -132,5 +136,6 @@ export const submitTypes = {
|
|||||||
[python]: 'tests',
|
[python]: 'tests',
|
||||||
[dialogue]: 'tests',
|
[dialogue]: 'tests',
|
||||||
[fillInTheBlank]: 'tests',
|
[fillInTheBlank]: 'tests',
|
||||||
[multifilePythonCertProject]: 'tests'
|
[multifilePythonCertProject]: 'tests',
|
||||||
|
[generic]: 'tests'
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user