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:
Tom
2024-10-18 06:03:20 -05:00
committed by GitHub
parent 66362872ed
commit 898b78c2de
10 changed files with 370 additions and 10 deletions

View File

@@ -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": [

View File

@@ -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.

View 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
}
}
}
}
`;

View File

@@ -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) {

View 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"
}

View File

@@ -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 `&amp;` and `&lt;`.
- **`<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.

View File

@@ -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),

View File

@@ -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),

View File

@@ -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(),

View File

@@ -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'
};