import { ObjectId } from 'bson'; import type { ChallengeLang } from '@freecodecamp/shared/config/curriculum'; const sanitizeTitle = (title: string) => { return title.includes(':') || title.includes("'") ? `"${title}"` : title; }; interface ChallengeOptions { challengeId: ObjectId; title: string; dashedName: string; challengeType: string; questionCount?: number; challengeLang?: ChallengeLang; inputType?: string; } const buildFrontMatter = ({ challengeId, title, dashedName, challengeType, challengeLang, inputType }: ChallengeOptions) => { const langString = challengeLang ? ` lang: ${challengeLang}` : ''; const inputTypeString = inputType ? ` inputType: ${inputType}` : ''; return `--- id: ${challengeId.toString()} title: ${sanitizeTitle(title)} challengeType: ${challengeType} dashedName: ${dashedName}${langString}${inputTypeString} ---`; }; const buildFrontMatterWithVideo = ({ challengeId, title, dashedName, challengeType }: ChallengeOptions) => `--- id: ${challengeId.toString()} videoId: ADD YOUR VIDEO ID HERE!!! title: ${sanitizeTitle(title)} challengeType: ${challengeType} dashedName: ${dashedName} ---`; const getLegacyChallengeTemplate = ( options: ChallengeOptions ): string => `${buildFrontMatter(options)} # --description-- ${options.title} description. # --instructions-- ${options.title} instructions. # --hints-- Test 1 \`\`\`js \`\`\` # --seed-- \`\`\`js \`\`\` # --solutions-- \`\`\`js \`\`\` `; const getQuizChallengeTemplate = (options: ChallengeOptions): string => { const count = options.questionCount!; const regularQuestion = `### --question-- #### --text-- Placeholder question #### --distractors-- Placeholder distractor 1 --- Placeholder distractor 2 --- Placeholder distractor 3 #### --answer-- Placeholder answer `; const questionWithAudio = `### --question-- #### --text-- Placeholder question #### --audio-- \`\`\`json { "audio": { "filename": "1.1-1.mp3", "startTimestamp": 5.7, "finishTimestamp": 6.48 }, "transcript": [ { "character": "Tom", "text": "Hi, I'm Tom." } ] } \`\`\` #### --distractors-- Placeholder distractor 1 --- Placeholder distractor 2 --- Placeholder distractor 3 #### --answer-- Placeholder answer `; const firstQuestion = options.challengeLang ? questionWithAudio : regularQuestion; return `${buildFrontMatter(options)} # --description-- To pass the quiz, you must correctly answer at least ${count == 20 ? '18' : '9'} of the ${count.toString()} questions below. # --quizzes-- ## --quiz-- ${firstQuestion}${regularQuestion.repeat(Math.max(0, count - 2))} ### --question-- #### --text-- Placeholder question #### --distractors-- Placeholder distractor 1 --- Placeholder distractor 2 --- Placeholder distractor 3 #### --answer-- Placeholder answer `; }; const getVideoChallengeTemplate = ( options: ChallengeOptions ): string => `${buildFrontMatterWithVideo(options)} # --description-- ${options.title} description. # --question-- ## --text-- ${options.title} question? ## --answers-- Answer 1 --- Answer 2 --- Answer 3 ## --video-solution-- 1 `; const getAssignmentChallengeTemplate = ( options: ChallengeOptions ): string => `${buildFrontMatter(options)} # --description-- ${options.title} description. # --assignment-- ${options.title} assignment! # --question-- ## --text-- ${options.title} question? ## --answers-- Answer 1 --- Answer 2 --- Answer 3 ## --video-solution-- 1 `; const getMultipleChoiceChallengeTemplate = ( options: ChallengeOptions ): string => { const correctAnswerIndex = Math.floor(Math.random() * 4) + 1; const feedback = (index: number) => index === correctAnswerIndex ? '' : ` ### --feedback-- Include feedback for answer ${index} here. `; const answers = [1, 2, 3, 4] .map( index => `Answer ${index} ${feedback(index)}` ) .join('\n---\n\n'); return `${buildFrontMatter(options)} # --description-- ${options.title} description. # --questions-- ## --text-- ${options.title} question? ## --answers-- ${answers} ## --video-solution-- ${correctAnswerIndex} `; }; const getFillInTheBlankChallengeTemplate = ( options: ChallengeOptions ): string => `${buildFrontMatter(options)} # --description-- ${options.title} description. # --fillInTheBlank-- ## --sentence-- \`Fill BLANK the BLANK sentence.\` ## --blanks-- \`in\` ### --feedback-- It's \`in\` --- \`blank\` `; const getDialogueChallengeTemplate = ( options: ChallengeOptions ): string => `${buildFrontMatter(options)} # --description-- Watch the video below to understand the context of the upcoming lessons. # --assignment-- Watch the video. # --scene-- \`\`\`json { "setup": { "background": "chaos.png", "characters": [ { "character": "David", "position": {"x":50,"y":80,"z":8}, "opacity": 0 } ], "audio": { "filename": "1.1-1.mp3", "startTime": 1, "startTimestamp": 5.7, "finishTimestamp": 6.48 } }, "commands": [ { "character": "David", "opacity": 1, "startTime": 0 }, { "character": "David", "startTime": 1, "finishTime": 0.78, "dialogue": { "text": "I'm Tom.", "align": "center" } }, { "character": "Tom", "opacity": 0, "startTime": 1.28 } ] } \`\`\` `; const getGenericChallengeTemplate = ( options: ChallengeOptions ): string => `${buildFrontMatter(options)} # --description-- Generic challenge description. # --assignment-- Do the assignment. `; interface DailyCodingChallengeOptions { challengeId: ObjectId; challengeNumber: number; } export const getDailyJavascriptChallengeTemplate = ({ challengeId, challengeNumber }: DailyCodingChallengeOptions) => `--- id: ${challengeId.toString()} title: "Challenge ${challengeNumber}: Placeholder" challengeType: 28 dashedName: challenge-${challengeNumber} --- # --description-- Placeholder description # --hints-- Placeholder test \`\`\`js assert.isTrue(true); \`\`\` # --seed-- ## --seed-contents-- \`\`\`js function placeholder(arg) { return arg; } \`\`\` # --solutions-- \`\`\`js function placeholder(arg) { return arg; } \`\`\` `; export const getDailyPythonChallengeTemplate = ({ challengeId, challengeNumber }: DailyCodingChallengeOptions) => `--- id: ${challengeId.toString()} title: "Challenge ${challengeNumber}: Placeholder" challengeType: 29 dashedName: challenge-${challengeNumber} --- # --description-- Placeholder description # --hints-- Placeholder test \`\`\`js ({test: () => { runPython(\` from unittest import TestCase TestCase().assertTrue(True)\`) }}) \`\`\` # --seed-- ## --seed-contents-- \`\`\`py def placeholder(arg): return arg \`\`\` # --solutions-- \`\`\`py def placeholder(arg): return arg \`\`\` `; type Template = (opts: ChallengeOptions) => string; export const getTemplate = (challengeType: string): Template => { const template = challengeTypeToTemplate[challengeType]; if (!template) { throw Error(`Challenge type ${challengeType} has no template. To create one, please add a new function to this file and include it in the challengeTypeToTemplate map. `); } return template; }; /** * This should be kept in parity with the challengeTypes in the * client. * * Keys are explicitly marked null so we know the challenge type itself * exists, and can expand this to use the correct template later on. */ const challengeTypeToTemplate: { [key: string]: null | Template; } = { 0: getLegacyChallengeTemplate, 1: getLegacyChallengeTemplate, 2: null, 3: getLegacyChallengeTemplate, 4: getLegacyChallengeTemplate, 5: getLegacyChallengeTemplate, 6: getLegacyChallengeTemplate, 7: null, 8: getQuizChallengeTemplate, 9: null, 10: null, 11: getVideoChallengeTemplate, 12: null, 13: null, 14: null, 15: getAssignmentChallengeTemplate, 16: null, 17: null, 18: null, 19: getMultipleChoiceChallengeTemplate, 20: null, 21: getDialogueChallengeTemplate, 22: getFillInTheBlankChallengeTemplate, 23: null, 24: getGenericChallengeTemplate };