mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-20 12:03:11 -04: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."]
|
||||
},
|
||||
"bzfv": { "title": "8", "intro": [] },
|
||||
"snuv": { "title": "9", "intro": [] },
|
||||
"review-basic-html": {
|
||||
"title": "Basic HTML Review",
|
||||
"intro": ["Review the basic HTML topics."]
|
||||
},
|
||||
"quiz-basic-html": {
|
||||
"title": "Basic HTML Quiz",
|
||||
"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.
|
||||
260
client/src/templates/Challenges/generic/show.tsx
Normal file
260
client/src/templates/Challenges/generic/show.tsx
Normal file
@@ -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'
|
||||
);
|
||||
|
||||
const generic = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/generic/show.tsx'
|
||||
);
|
||||
|
||||
const views = {
|
||||
backend,
|
||||
classic,
|
||||
@@ -73,8 +78,8 @@ const views = {
|
||||
exam,
|
||||
msTrophy,
|
||||
dialogue,
|
||||
fillInTheBlank
|
||||
// quiz: Quiz
|
||||
fillInTheBlank,
|
||||
generic
|
||||
};
|
||||
|
||||
function getIsFirstStepInBlock(id, edges) {
|
||||
|
||||
10
curriculum/challenges/_meta/review-basic-html/meta.json
Normal file
10
curriculum/challenges/_meta/review-basic-html/meta.json
Normal file
@@ -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"
|
||||
}
|
||||
@@ -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(),
|
||||
certification: Joi.string().regex(slugWithSlashRE),
|
||||
challengeType: Joi.number().min(0).max(23).required(),
|
||||
challengeType: Joi.number().min(0).max(24).required(),
|
||||
checksum: Joi.number(),
|
||||
// TODO: require this only for normal challenges, not certs
|
||||
dashedName: Joi.string().regex(slugRE),
|
||||
|
||||
@@ -140,7 +140,7 @@ const schema = Joi.object()
|
||||
}),
|
||||
challengeOrder: Joi.number(),
|
||||
certification: Joi.string().regex(slugWithSlashRE),
|
||||
challengeType: Joi.number().min(0).max(23).required(),
|
||||
challengeType: Joi.number().min(0).max(24).required(),
|
||||
checksum: Joi.number(),
|
||||
// TODO: require this only for normal challenges, not certs
|
||||
dashedName: Joi.string().regex(slugRE),
|
||||
|
||||
@@ -6,13 +6,20 @@ const slugWithSlashRE = new RegExp('^[a-z0-9-/]+$');
|
||||
const schema = Joi.object()
|
||||
.keys({
|
||||
name: Joi.string().required(),
|
||||
blockType: Joi.valid('workshop', 'lab', 'lecture', 'quiz', 'exam'),
|
||||
blockLayout: Joi.valid(
|
||||
'challenge-list',
|
||||
'challenge-grid',
|
||||
'link',
|
||||
'project-list'
|
||||
),
|
||||
blockType: Joi.valid(
|
||||
'workshop',
|
||||
'lab',
|
||||
'lecture',
|
||||
'review',
|
||||
'quiz',
|
||||
'exam'
|
||||
),
|
||||
isUpcomingChange: Joi.boolean().required(),
|
||||
dashedName: Joi.string().regex(slugRE).required(),
|
||||
superBlock: Joi.string().regex(slugWithSlashRE).required(),
|
||||
|
||||
@@ -23,6 +23,7 @@ const python = 20;
|
||||
const dialogue = 21;
|
||||
const fillInTheBlank = 22;
|
||||
const multifilePythonCertProject = 23;
|
||||
const generic = 24;
|
||||
|
||||
export const challengeTypes = {
|
||||
html,
|
||||
@@ -49,7 +50,8 @@ export const challengeTypes = {
|
||||
python,
|
||||
dialogue,
|
||||
fillInTheBlank,
|
||||
multifilePythonCertProject
|
||||
multifilePythonCertProject,
|
||||
generic
|
||||
};
|
||||
|
||||
export const hasNoSolution = (challengeType: number): boolean => {
|
||||
@@ -71,7 +73,8 @@ export const hasNoSolution = (challengeType: number): boolean => {
|
||||
msTrophy,
|
||||
multipleChoice,
|
||||
dialogue,
|
||||
fillInTheBlank
|
||||
fillInTheBlank,
|
||||
generic
|
||||
];
|
||||
|
||||
return noSolutions.includes(challengeType);
|
||||
@@ -101,7 +104,8 @@ export const viewTypes = {
|
||||
[python]: 'modern',
|
||||
[dialogue]: 'dialogue',
|
||||
[fillInTheBlank]: 'fillInTheBlank',
|
||||
[multifilePythonCertProject]: 'classic'
|
||||
[multifilePythonCertProject]: 'classic',
|
||||
[generic]: 'generic'
|
||||
};
|
||||
|
||||
// determine the type of submit function to use for the challenge on completion
|
||||
@@ -132,5 +136,6 @@ export const submitTypes = {
|
||||
[python]: 'tests',
|
||||
[dialogue]: 'tests',
|
||||
[fillInTheBlank]: 'tests',
|
||||
[multifilePythonCertProject]: 'tests'
|
||||
[multifilePythonCertProject]: 'tests',
|
||||
[generic]: 'tests'
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user