feat(client): show demo on demand in labs (#55569)

Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
Co-authored-by: Tom <20648924+moT01@users.noreply.github.com>
This commit is contained in:
Oliver Eyton-Williams
2024-08-13 14:56:19 +02:00
committed by GitHub
parent 7b05b89d03
commit b9893bb4d6
51 changed files with 108 additions and 44 deletions

View File

@@ -80,6 +80,7 @@ exports.createPages = async function createPages({
certification
challengeType
dashedName
demoType
disableLoopProtectTests
disableLoopProtectPreview
fields {

View File

@@ -67,6 +67,7 @@
"click-here": "Click here to sign in",
"save": "Save",
"save-code": "Save your Code",
"show-demo": "Show Demo",
"no-thanks": "No thanks",
"yes-please": "Yes please",
"update-email": "Update my Email",

View File

@@ -213,7 +213,6 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
challengeData={challengeData}
closeText={t('buttons.close')}
previewTitle={projectTitle}
showProjectPreview={true}
/>
<ExamResultsModal projectTitle={projectTitle} examResults={examResults} />

View File

@@ -229,7 +229,6 @@ function TimelineInner({
challengeData={challengeData}
closeText={t('buttons.close')}
previewTitle={projectTitle}
showProjectPreview={true}
/>
<ExamResultsModal
projectTitle={projectTitle}

View File

@@ -461,7 +461,6 @@ function CertificationSettings(props: CertificationSettingsProps) {
challengeData={challengeData}
previewTitle={projectTitle}
closeText={t('buttons.close')}
showProjectPreview={true}
/>
<ExamResultsModal projectTitle={projectTitle} examResults={examResults} />
</section>

View File

@@ -184,6 +184,7 @@ export type ChallengeNode = {
challengeOrder: number;
challengeType: number;
dashedName: string;
demoType: 'onClick' | 'onLoad' | null;
description: string;
challengeFiles: ChallengeFiles;
fields: Fields;

View File

@@ -110,7 +110,6 @@ interface ShowClassicProps extends Pick<PreviewProps, 'previewMounted'> {
challengeMeta: ChallengeMeta;
projectPreview: {
challengeData: CompletedChallenge;
showProjectPreview: boolean;
};
};
updateChallengeMeta: (arg0: ChallengeMeta) => void;
@@ -183,6 +182,7 @@ function ShowClassic({
challenge: {
challengeFiles: seedChallengeFiles,
block,
demoType,
title,
description,
instructions,
@@ -202,7 +202,7 @@ function ShowClassic({
pageContext: {
challengeMeta,
challengeMeta: { isFirstStep, nextChallengePath, prevChallengePath },
projectPreview: { challengeData, showProjectPreview }
projectPreview: { challengeData }
},
createFiles,
cancelTests,
@@ -359,7 +359,10 @@ function ShowClassic({
);
initTests(tests);
if (showProjectPreview) openModal('projectPreview');
// Typically, this kind of preview only appears on the first step of a
// project and is shown (once) automatically. In contrast, labs are more
// freeform, so the preview is shown on demand.
if (demoType === 'onLoad') openModal('projectPreview');
updateChallengeMeta({
...challengeMeta,
title,
@@ -371,9 +374,11 @@ function ShowClassic({
};
const renderInstructionsPanel = ({
toolPanel
toolPanel,
hasDemo
}: {
toolPanel: React.ReactNode;
hasDemo: boolean;
}) => {
return (
<SidePanel
@@ -397,6 +402,7 @@ function ShowClassic({
instructionsPanelRef={instructionsPanelRef}
toolPanel={toolPanel}
superBlock={superBlock}
hasDemo={hasDemo}
/>
);
};
@@ -420,7 +426,7 @@ function ShowClassic({
resizeProps={resizeProps}
title={title}
usesMultifileEditor={usesMultifileEditor}
showProjectPreview={showProjectPreview}
showProjectPreview={demoType === 'onLoad'}
/>
)
);
@@ -448,7 +454,8 @@ function ShowClassic({
hasEditableBoundaries={hasEditableBoundaries}
hasPreview={showPreview}
instructions={renderInstructionsPanel({
toolPanel: null
toolPanel: null,
hasDemo: demoType === 'onClick'
})}
notes={notes}
onPreviewResize={onPreviewResize}
@@ -482,7 +489,8 @@ function ShowClassic({
hasEditableBoundaries={hasEditableBoundaries}
hasPreview={showPreview}
instructions={renderInstructionsPanel({
toolPanel: <ToolPanel guideUrl={guideUrl} videoUrl={videoUrl} />
toolPanel: <ToolPanel guideUrl={guideUrl} videoUrl={videoUrl} />,
hasDemo: demoType === 'onClick'
})}
isFirstStep={isFirstStep}
layoutState={layout}
@@ -512,7 +520,6 @@ function ShowClassic({
challengeData={challengeData}
closeText={t('buttons.start-coding')}
previewTitle={t('learn.project-preview-title')}
showProjectPreview={showProjectPreview}
/>
<ShortcutsModal />
</LearnLayout>
@@ -529,6 +536,7 @@ export const query = graphql`
challengeNode(challenge: { fields: { slug: { eq: $slug } } }) {
challenge {
block
demoType
title
description
id

View File

@@ -16,7 +16,6 @@ import './project-preview-modal.css';
interface ProjectPreviewMountedPayload {
challengeData: CompletedChallenge | null;
showProjectPreview: boolean;
}
interface Props {
@@ -25,7 +24,6 @@ interface Props {
projectPreviewMounted: (payload: ProjectPreviewMountedPayload) => void;
challengeData: CompletedChallenge | null;
setEditorFocusability: (focusability: boolean) => void;
showProjectPreview: boolean;
previewTitle: string;
closeText: string;
}
@@ -45,7 +43,6 @@ function ProjectPreviewModal({
projectPreviewMounted,
challengeData,
setEditorFocusability,
showProjectPreview,
previewTitle,
closeText
}: Props): JSX.Element {
@@ -66,9 +63,7 @@ function ProjectPreviewModal({
<Modal.Body>
<Preview
previewId={projectPreviewId}
previewMounted={() =>
projectPreviewMounted({ challengeData, showProjectPreview })
}
previewMounted={() => projectPreviewMounted({ challengeData })}
/>
</Modal.Body>
<Modal.Footer>

View File

@@ -1,11 +1,15 @@
import React, { useEffect, ReactElement, ReactNode } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Test } from '../../../redux/prop-types';
import { useTranslation } from 'react-i18next';
import { Button } from '@freecodecamp/ui';
import { Test } from '../../../redux/prop-types';
import { SuperBlocks } from '../../../../../shared/config/curriculum';
import { initializeMathJax } from '../../../utils/math-jax';
import { challengeTestsSelector } from '../redux/selectors';
import { openModal } from '../redux/actions';
import { Spacer } from '../../../components/helpers';
import TestSuite from './test-suite';
import './side-panel.css';
@@ -16,11 +20,22 @@ const mapStateToProps = createSelector(
tests
})
);
interface SidePanelProps {
const mapDispatchToProps: {
openModal: (modal: string) => void;
} = {
openModal
};
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
interface SidePanelProps extends DispatchProps, StateProps {
block: string;
challengeDescription: ReactElement;
challengeTitle: ReactElement;
instructionsPanelRef: React.RefObject<HTMLDivElement>;
hasDemo: boolean;
toolPanel: ReactNode;
superBlock: SuperBlocks;
tests: Test[];
@@ -31,10 +46,13 @@ export function SidePanel({
challengeDescription,
challengeTitle,
instructionsPanelRef,
hasDemo,
toolPanel,
superBlock,
tests
tests,
openModal
}: SidePanelProps): JSX.Element {
const { t } = useTranslation();
useEffect(() => {
const mathJaxChallenge =
superBlock === SuperBlocks.RosettaCode ||
@@ -50,6 +68,14 @@ export function SidePanel({
tabIndex={-1}
>
{challengeTitle}
{hasDemo && (
<>
<Button size='small' onClick={() => openModal('projectPreview')}>
{t('buttons.show-demo')}
</Button>
<Spacer size='xSmall' />
</>
)}
{challengeDescription}
{toolPanel}
<TestSuite tests={tests} />
@@ -59,4 +85,4 @@ export function SidePanel({
SidePanel.displayName = 'SidePanel';
export default connect(mapStateToProps)(SidePanel);
export default connect(mapStateToProps, mapDispatchToProps)(SidePanel);

View File

@@ -348,9 +348,8 @@ function* updatePython(challengeData) {
}
function* previewProjectSolutionSaga({ payload }) {
if (!payload) return;
const { showProjectPreview, challengeData } = payload;
if (!showProjectPreview) return;
if (!payload?.challengeData) return;
const { challengeData } = payload;
try {
if (canBuildChallenge(challengeData)) {

View File

@@ -1,9 +1,6 @@
const path = require('path');
const { sortChallengeFiles } = require('../sort-challengefiles');
const {
challengeTypes,
viewTypes
} = require('../../../shared/config/challenge-types');
const { viewTypes } = require('../../../shared/config/challenge-types');
const backend = path.resolve(
__dirname,
@@ -150,8 +147,7 @@ exports.createChallengePages = function (createPage) {
// it during the curriculum build process and attach it to the first challenge?
// That would remove the need to analyse allChallengeEdges.
function getProjectPreviewConfig(challenge, allChallengeEdges) {
const { block, challengeOrder, challengeType, usesMultifileEditor } =
challenge;
const { block } = challenge;
const challengesInBlock = allChallengeEdges
.filter(({ node: { challenge } }) => challenge.block === block)
@@ -169,15 +165,6 @@ function getProjectPreviewConfig(challenge, allChallengeEdges) {
}));
return {
showProjectPreview:
challengeOrder === 0 &&
usesMultifileEditor &&
// TODO: handle the special cases better. Create a meta property for
// showProjectPreview, maybe? Then we can remove all the following cases
challengeType !== challengeTypes.multifileCertProject &&
challengeType !== challengeTypes.multifilePythonCertProject &&
challengeType !== challengeTypes.python &&
challengeType !== challengeTypes.js,
challengeData: {
challengeType: lastChallenge.challengeType,
challengeFiles: projectPreviewChallengeFiles

View File

@@ -3,6 +3,7 @@ id: 5d8a4cfbe6b6180ed9a1c9de
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 614ccc21ea91ef1736b9b578
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 5f33071498eb2472b87ddee4
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 6140c7e645d8e905819f1dd4
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 61695197ac34f0407e339882
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 61537485c4f2a624f18d7794
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 61437d575fb98f57fa8f7f36
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 619665c9abd72906f3ad30f9
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 5d822fd413a79914d39e98c9
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 5dc174fcf86c76b9248c6eb2
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 60eebd07ea685b0e777b5583
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 646c47867800472a4ed5d2ea
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 61fd5a93fd62bb35968adeab
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 612e6afc009b450a437940a1
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 60a3e3396c7b40068ad6996a
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 615f2abbe7d18d49a1e0e1c8
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 635060a5c03c950f46174cb5
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 657386f11fb8265660bfac75
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 64061a98f704a014b44afdb2
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 6650c9a94d6e13d14a043a69
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 5d5a813321b9e3db6c106a46
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 63ec14d1c216aa063f0be4af
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 652f948489abbb81e6bf5a01
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 641d9a19bff38d34d5a5edb8
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 5ddb965c65d27e1512d44d9a
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 642db8c409d9991d0b3b2f0d
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 6461815bc48998eb15d55349
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 64e4e4c4ec263b62ae7bf54d
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 63c620161fc2b49ac340ffc4
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 63db7f4677d06d7500a13321
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 641cd18eb67c661d8a9e11f3
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 65386e889dd615940cb3e042
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 657a0ea50da0c8d9d6d7950a
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 663d0ab797cb716189ffcc0a
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--

View File

@@ -3,6 +3,7 @@ id: 668f08ea07b99b1f4a91acab
title: Build a Recipe Page
challengeType: 14
dashedName: build-a-recipe-page
demoType: onClick
---
# --description--

View File

@@ -107,6 +107,7 @@ const schema = Joi.object()
checksum: Joi.number(),
// TODO: require this only for normal challenges, not certs
dashedName: Joi.string().regex(slugRE),
demoType: Joi.string().valid('onClick', 'onLoad'),
description: Joi.when('challengeType', {
is: [
challengeTypes.step,

View File

@@ -104,6 +104,7 @@ const schema = Joi.object()
checksum: Joi.number(),
// TODO: require this only for normal challenges, not certs
dashedName: Joi.string().regex(slugRE),
demoType: Joi.string().valid('onClick', 'onLoad'),
description: Joi.when('challengeType', {
is: [
challengeTypes.step,

View File

@@ -163,7 +163,8 @@ async function createFirstChallenge(
projectPath: newChallengeDir + '/',
stepNum: 1,
challengeType: 0,
challengeSeeds
challengeSeeds,
isFirstChallenge: true
});
}

View File

@@ -25,6 +25,7 @@ type StepOptions = {
challengeSeeds: Record<string, ChallengeSeed>;
stepNum: number;
challengeType: number;
isFirstChallenge?: boolean;
};
export interface ChallengeSeed {
@@ -40,7 +41,8 @@ function getStepTemplate({
challengeId,
challengeSeeds,
stepNum,
challengeType
challengeType,
isFirstChallenge = false
}: StepOptions): string {
const seedTexts = Object.values(challengeSeeds)
.map(({ contents, ext, editableRegionBoundaries }: ChallengeSeed) => {
@@ -67,12 +69,18 @@ function getStepTemplate({
const seedHeadSection = getSeedSection(seedHeads, 'before-user-code');
const seedTailSection = getSeedSection(seedTails, 'after-user-code');
const demoString = isFirstChallenge
? `
# demoType can either be 'onClick' or 'onLoad'. If the project or lab doesn't have a preview, delete the property
demoType: onClick`
: '';
return (
`---
id: ${challengeId.toString()}
title: Step ${stepNum}
challengeType: ${challengeType}
dashedName: step-${stepNum}
dashedName: step-${stepNum}${demoString}
---
# --description--

View File

@@ -16,13 +16,15 @@ interface Options {
challengeType: number;
projectPath?: string;
challengeSeeds?: Record<string, ChallengeSeed>;
isFirstChallenge?: boolean;
}
const createStepFile = ({
stepNum,
challengeType,
projectPath = getProjectPath(),
challengeSeeds = {}
challengeSeeds = {},
isFirstChallenge = false
}: Options): ObjectID => {
const challengeId = new ObjectID();
@@ -30,7 +32,8 @@ const createStepFile = ({
challengeId,
challengeSeeds,
stepNum,
challengeType
challengeType,
isFirstChallenge
});
// eslint-disable-next-line @typescript-eslint/no-base-to-string