diff --git a/client/schema.gql b/client/schema.gql index 92d3f4f5966..bdda699158a 100644 --- a/client/schema.gql +++ b/client/schema.gql @@ -351,10 +351,27 @@ type Quiz { questions: [QuizQuestion] } +type QuizAudio { + filename: String + startTimestamp: Float + finishTimestamp: Float +} + +type QuizTranscriptLine { + character: String + text: String +} + +type QuizAudioData { + audio: QuizAudio + transcript: [QuizTranscriptLine] +} + type QuizQuestion { text: String distractors: [String] answer: String + audioData: QuizAudioData } type RequiredResource { diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index fc3ca4b261e..87ce096a932 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -347,10 +347,27 @@ type Quiz = { questions: QuizQuestion[]; }; +type QuizAudio = { + filename: string; + startTimestamp?: number | null; + finishTimestamp?: number | null; +}; + +type QuizTranscriptLine = { + character: string; + text: string; +}; + +type QuizAudioData = { + audio: QuizAudio; + transcript: QuizTranscriptLine[]; +}; + type QuizQuestion = { text: string; distractors: string[]; answer: string; + audioData?: QuizAudioData | null; }; export type CertificateNode = { diff --git a/client/src/templates/Challenges/quiz/show.css b/client/src/templates/Challenges/quiz/show.css index d96f1c89f65..8dd852f1417 100644 --- a/client/src/templates/Challenges/quiz/show.css +++ b/client/src/templates/Challenges/quiz/show.css @@ -25,3 +25,9 @@ .quiz-answer-label:has(ruby) { line-height: 1.7rem; } + +/* Override global button:hover for the play/pause button in the Quiz component */ +.quiz-challenge-container button:has(svg.svg-inline--fa):hover { + background-color: unset; + color: unset; +} diff --git a/client/src/templates/Challenges/quiz/show.tsx b/client/src/templates/Challenges/quiz/show.tsx index 61ce5ba7952..2ad31145521 100644 --- a/client/src/templates/Challenges/quiz/show.tsx +++ b/client/src/templates/Challenges/quiz/show.tsx @@ -166,16 +166,36 @@ const ShowQuiz = ({ value: 4 }; - return { + const allAnswers = shuffleArray([...distractors, answer]); + + const audioData = question.audioData?.audio?.filename + ? { + audioUrl: `https://cdn.freecodecamp.org/curriculum/english/animation-assets/sounds/${question.audioData.audio.filename}`, + audioStartTime: + question.audioData.audio.startTimestamp ?? undefined, + audioFinishTime: + question.audioData.audio.finishTimestamp ?? undefined, + transcript: question.audioData.transcript.length + ? question.audioData.transcript + .map(line => `

${line.character}: ${line.text}

`) + .join('') + : undefined + } + : {}; + + const questionData = { question: ( ), - answers: shuffleArray([...distractors, answer]), - correctAnswer: answer.value + answers: allAnswers, + correctAnswer: answer.value, + ...audioData }; + + return questionData; }) ); @@ -400,6 +420,17 @@ export const query = graphql` distractors text answer + audioData { + audio { + filename + startTimestamp + finishTimestamp + } + transcript { + character + text + } + } } } tests { diff --git a/curriculum/challenges/english/blocks/en-a2-quiz-greetings-first-day-office/69601914119b98f0f0c530b8.md b/curriculum/challenges/english/blocks/en-a2-quiz-greetings-first-day-office/69601914119b98f0f0c530b8.md index 56a80b068fa..2af9ab6b09d 100644 --- a/curriculum/challenges/english/blocks/en-a2-quiz-greetings-first-day-office/69601914119b98f0f0c530b8.md +++ b/curriculum/challenges/english/blocks/en-a2-quiz-greetings-first-day-office/69601914119b98f0f0c530b8.md @@ -44,7 +44,25 @@ Which of the following is NOT a greeting? #### --text-- -Which is the correct reply to this sentence: `Hello! You're the new graphic designer, right?` +Listen to the audio and select the most appropriate answer. + +#### --audio-- + +```json +{ + "audio": { + "filename": "1.1-1.mp3", + "startTimestamp": 0, + "finishTimestamp": 4.2 + }, + "transcript": [ + { + "character": "Maria", + "text": "Hello. You're the new graphic designer, right? I'm Maria, the team lead." + } + ] +} +``` #### --distractors-- diff --git a/e2e/fixtures/quiz-audio-fixture.json b/e2e/fixtures/quiz-audio-fixture.json new file mode 100644 index 00000000000..7887ce96d5d --- /dev/null +++ b/e2e/fixtures/quiz-audio-fixture.json @@ -0,0 +1,24 @@ +[ + { + "questions": [ + { + "distractors": ["

Wrong 1

", "

Wrong 2

", "

Wrong 3

"], + "text": "

What does the audio say?

", + "answer": "

Correct answer

", + "audioData": { + "audio": { + "filename": "test-audio.mp3", + "startTimestamp": 0, + "finishTimestamp": 2 + }, + "transcript": [ + { + "character": "Speaker", + "text": "Hello world" + } + ] + } + } + ] + } +] diff --git a/e2e/quiz-challenge.spec.ts b/e2e/quiz-challenge.spec.ts index aaa64cff0c7..36fe933c4be 100644 --- a/e2e/quiz-challenge.spec.ts +++ b/e2e/quiz-challenge.spec.ts @@ -289,3 +289,46 @@ test.describe('Quiz challenge', () => { await page.close({ runBeforeUnload: true }); }); }); + +test.describe('Quiz with audio question', () => { + test.beforeEach(async ({ page }) => { + const fixturePath = path.join( + __dirname, + 'fixtures', + 'quiz-audio-fixture.json' + ); + const fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf8')) as Quiz[]; + + // Intercept the exact page-data.json for the quiz and inject the fixture + await page.route(`**/page-data${quizPath}/page-data.json`, async route => { + const response = await route.fetch(); + const body = await response.text(); + + const pageData = JSON.parse(body) as PageData; + pageData.result.data.challengeNode.challenge.quizzes = fixture; + + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify(pageData) + }); + }); + + await page.goto(quizPath); + }); + + test('renders audio player and transcript when question has audio', async ({ + page + }) => { + await expect(page.getByRole('radiogroup')).toHaveCount(1); + + const audio = page.locator('audio'); + await expect(audio).toHaveCount(1); + await expect(audio).toHaveAttribute( + 'src', + 'https://cdn.freecodecamp.org/curriculum/english/animation-assets/sounds/test-audio.mp3' + ); + + await page.getByText(/transcript/i).click(); + await expect(page.getByText('Speaker: Hello world')).toBeVisible(); + }); +}); diff --git a/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-empty-transcript.md b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-empty-transcript.md new file mode 100644 index 00000000000..c7a302c0454 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-empty-transcript.md @@ -0,0 +1,36 @@ +# --quizzes-- + +## --quiz-- + +### --question-- + +#### --text-- + +Question with audio but empty transcript + +#### --audio-- + +```json +{ + "audio": { + "filename": "audio-with-empty-transcript.mp3" + }, + "transcript": [] +} +``` + +#### --distractors-- + +Distractor 1 + +--- + +Distractor 2 + +--- + +Distractor 3 + +#### --answer-- + +Answer \ No newline at end of file diff --git a/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-invalid-json.md b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-invalid-json.md new file mode 100644 index 00000000000..01229248f61 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-invalid-json.md @@ -0,0 +1,42 @@ +# --quizzes-- + +## --quiz-- + +### --question-- + +#### --text-- + +Question with invalid JSON audio + +#### --audio-- + +```json +{ + "audio": { + "filename": "audio.mp3" + }, + "transcript": [ + { + "character": "A", + "text": "Hello" + } + ] + // missing comma +} +``` + +#### --distractors-- + +Distractor 1 + +--- + +Distractor 2 + +--- + +Distractor 3 + +#### --answer-- + +Answer \ No newline at end of file diff --git a/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-missing-filename.md b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-missing-filename.md new file mode 100644 index 00000000000..5627eb68de7 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-missing-filename.md @@ -0,0 +1,39 @@ +# --quizzes-- + +## --quiz-- + +### --question-- + +#### --text-- + +Question with audio missing filename + +#### --audio-- + +```json +{ + "audio": {}, + "transcript": [ + { + "character": "A", + "text": "Hello" + } + ] +} +``` + +#### --distractors-- + +Distractor 1 + +--- + +Distractor 2 + +--- + +Distractor 3 + +#### --answer-- + +Answer \ No newline at end of file diff --git a/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-missing-transcript.md b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-missing-transcript.md new file mode 100644 index 00000000000..4906f8185c7 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-missing-transcript.md @@ -0,0 +1,35 @@ +# --quizzes-- + +## --quiz-- + +### --question-- + +#### --text-- + +Question with audio but no transcript + +#### --audio-- + +```json +{ + "audio": { + "filename": "audio-without-transcript.mp3" + } +} +``` + +#### --distractors-- + +Distractor 1 + +--- + +Distractor 2 + +--- + +Distractor 3 + +#### --answer-- + +Answer \ No newline at end of file diff --git a/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-not-json-code-block.md b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-not-json-code-block.md new file mode 100644 index 00000000000..3efb5c1b5a1 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-not-json-code-block.md @@ -0,0 +1,29 @@ +# --quizzes-- + +## --quiz-- + +### --question-- + +#### --text-- + +Question with audio not in JSON code block + +#### --audio-- + +Some plain text audio data + +#### --distractors-- + +Distractor 1 + +--- + +Distractor 2 + +--- + +Distractor 3 + +#### --answer-- + +Answer \ No newline at end of file diff --git a/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-transcript-missing-character.md b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-transcript-missing-character.md new file mode 100644 index 00000000000..66e5be5fab3 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-transcript-missing-character.md @@ -0,0 +1,40 @@ +# --quizzes-- + +## --quiz-- + +### --question-- + +#### --text-- + +Question with audio transcript line missing character + +#### --audio-- + +```json +{ + "audio": { + "filename": "audio.mp3" + }, + "transcript": [ + { + "text": "Hello" + } + ] +} +``` + +#### --distractors-- + +Distractor 1 + +--- + +Distractor 2 + +--- + +Distractor 3 + +#### --answer-- + +Answer \ No newline at end of file diff --git a/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-transcript-missing-text.md b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-transcript-missing-text.md new file mode 100644 index 00000000000..c60318b67bd --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-transcript-missing-text.md @@ -0,0 +1,40 @@ +# --quizzes-- + +## --quiz-- + +### --question-- + +#### --text-- + +Question with audio transcript line missing text + +#### --audio-- + +```json +{ + "audio": { + "filename": "audio.mp3" + }, + "transcript": [ + { + "character": "A" + } + ] +} +``` + +#### --distractors-- + +Distractor 1 + +--- + +Distractor 2 + +--- + +Distractor 3 + +#### --answer-- + +Answer \ No newline at end of file diff --git a/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-transcript-not-array.md b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-transcript-not-array.md new file mode 100644 index 00000000000..cf4fbaed06d --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio-transcript-not-array.md @@ -0,0 +1,36 @@ +# --quizzes-- + +## --quiz-- + +### --question-- + +#### --text-- + +Question with audio transcript not array + +#### --audio-- + +```json +{ + "audio": { + "filename": "audio.mp3" + }, + "transcript": "not an array" +} +``` + +#### --distractors-- + +Distractor 1 + +--- + +Distractor 2 + +--- + +Distractor 3 + +#### --answer-- + +Answer \ No newline at end of file diff --git a/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio.md b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio.md new file mode 100644 index 00000000000..0c2d4e55aea --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-quizzes-audio.md @@ -0,0 +1,107 @@ +# --quizzes-- + +## --quiz-- + +### --question-- + +#### --text-- + +Quiz 1, question 1 with audio timestamps + +#### --audio-- + +```json +{ + "audio": { + "filename": "audio-with-timestamps.mp3", + "startTimestamp": 1.5, + "finishTimestamp": 3.8 + }, + "transcript": [ + { + "character": "Maria", + "text": "Hello, how are you?" + }, + { + "character": "Tom", + "text": "I'm doing well, thank you." + } + ] +} +``` + +#### --distractors-- + +Quiz 1, question 1, distractor 1 + +--- + +Quiz 1, question 1, distractor 2 + +--- + +Quiz 1, question 1, distractor 3 + +#### --answer-- + +Quiz 1, question 1, answer + +### --question-- + +#### --text-- + +Quiz 1, question 2 with audio but no timestamps + +#### --audio-- + +```json +{ + "audio": { + "filename": "audio-full-file.mp3" + }, + "transcript": [ + { + "character": "Speaker", + "text": "This is the full audio transcript." + } + ] +} +``` + +#### --distractors-- + +Quiz 1, question 2, distractor 1 + +--- + +Quiz 1, question 2, distractor 2 + +--- + +Quiz 1, question 2, distractor 3 + +#### --answer-- + +Quiz 1, question 2, answer + +### --question-- + +#### --text-- + +Quiz 1, question 3 without audio + +#### --distractors-- + +Quiz 1, question 3, distractor 1 + +--- + +Quiz 1, question 3, distractor 2 + +--- + +Quiz 1, question 3, distractor 3 + +#### --answer-- + +Quiz 1, question 3, answer diff --git a/tools/challenge-parser/parser/plugins/add-quizzes.js b/tools/challenge-parser/parser/plugins/add-quizzes.js index defca36581e..e895eab9a63 100644 --- a/tools/challenge-parser/parser/plugins/add-quizzes.js +++ b/tools/challenge-parser/parser/plugins/add-quizzes.js @@ -21,7 +21,7 @@ function plugin() { }); } - function getQuestion(textNodes, distractorNodes, answerNodes) { + function getQuestion(textNodes, distractorNodes, answerNodes, audioNodes) { const text = toHtml(textNodes); const distractors = getDistractors(distractorNodes); const answer = toHtml(answerNodes); @@ -31,7 +31,52 @@ function plugin() { throw Error('--distractors-- are missing from quiz question'); if (!answer) throw Error('--answer-- is missing from quiz question'); - return { text, distractors, answer }; + const questionData = { text, distractors, answer }; + + // Extract audio data if present + if (audioNodes.length > 0) { + // Audio should be in a JSON code block + if (audioNodes[0].type !== 'code' || audioNodes[0].lang !== 'json') { + throw Error('--audio-- section must contain a ```json code block'); + } + + try { + const audioData = JSON.parse(audioNodes[0].value); + + // Validate structure + if (!audioData.audio || !audioData.audio.filename) { + throw Error('--audio-- section must contain audio.filename'); + } + + if (!audioData.transcript || !Array.isArray(audioData.transcript)) { + throw Error( + '--audio-- section must contain transcript as an array' + ); + } + + if (audioData.transcript.length === 0) { + throw Error('--audio-- section transcript array cannot be empty'); + } + + // Validate each transcript line + audioData.transcript.forEach((line, index) => { + if (!line.character || !line.text) { + throw Error( + `--audio-- transcript line ${index} must have character and text properties` + ); + } + }); + + questionData.audioData = audioData; + } catch (error) { + if (error instanceof SyntaxError) { + throw Error('--audio-- section must contain valid JSON'); + } + throw error; + } + } + + return questionData; } if (quizzesNodes.length > 0) { @@ -60,9 +105,10 @@ function plugin() { const textNodes = getSection(questionTree, '--text--'); const distractorNodes = getSection(questionTree, '--distractors--'); const answerNodes = getSection(questionTree, '--answer--'); + const audioNodes = getSection(questionTree, '--audio--'); quizQuestions.push( - getQuestion(textNodes, distractorNodes, answerNodes) + getQuestion(textNodes, distractorNodes, answerNodes, audioNodes) ); }); diff --git a/tools/challenge-parser/parser/plugins/add-quizzes.test.js b/tools/challenge-parser/parser/plugins/add-quizzes.test.js index e722bd0924d..2b831301c38 100644 --- a/tools/challenge-parser/parser/plugins/add-quizzes.test.js +++ b/tools/challenge-parser/parser/plugins/add-quizzes.test.js @@ -5,12 +5,46 @@ import addQuizzes from './add-quizzes'; describe('add-quizzes plugin', () => { let mockQuizzesAST; let chineseQuizzesAST; + let quizzesWithAudioAST; + let quizzesWithAudioInvalidJsonAST; + let quizzesWithAudioMissingFilenameAST; + let quizzesWithAudioTranscriptNotArrayAST; + let quizzesWithAudioEmptyTranscriptAST; + let quizzesWithAudioMissingTranscriptAST; + let quizzesWithAudioTranscriptMissingCharacterAST; + let quizzesWithAudioTranscriptMissingTextAST; + let quizzesWithAudioNotJsonCodeBlockAST; const plugin = addQuizzes(); let file = { data: {} }; beforeAll(async () => { mockQuizzesAST = await parseFixture('with-quizzes.md'); chineseQuizzesAST = await parseFixture('with-chinese-quizzes.md'); + quizzesWithAudioAST = await parseFixture('with-quizzes-audio.md'); + quizzesWithAudioInvalidJsonAST = await parseFixture( + 'with-quizzes-audio-invalid-json.md' + ); + quizzesWithAudioMissingFilenameAST = await parseFixture( + 'with-quizzes-audio-missing-filename.md' + ); + quizzesWithAudioTranscriptNotArrayAST = await parseFixture( + 'with-quizzes-audio-transcript-not-array.md' + ); + quizzesWithAudioEmptyTranscriptAST = await parseFixture( + 'with-quizzes-audio-empty-transcript.md' + ); + quizzesWithAudioMissingTranscriptAST = await parseFixture( + 'with-quizzes-audio-missing-transcript.md' + ); + quizzesWithAudioTranscriptMissingCharacterAST = await parseFixture( + 'with-quizzes-audio-transcript-missing-character.md' + ); + quizzesWithAudioTranscriptMissingTextAST = await parseFixture( + 'with-quizzes-audio-transcript-missing-text.md' + ); + quizzesWithAudioNotJsonCodeBlockAST = await parseFixture( + 'with-quizzes-audio-not-json-code-block.md' + ); }); beforeEach(() => { @@ -135,4 +169,111 @@ describe('add-quizzes plugin', () => { '

Quiz 1, question 3, answer with zhōng wén

' ); }); + + it('should parse audio sections in quiz questions', () => { + plugin(quizzesWithAudioAST, file); + const quizzes = file.data.quizzes; + + expect(Array.isArray(quizzes)).toBe(true); + expect(quizzes.length).toBe(1); + + const firstQuiz = quizzes[0]; + const firstQuestion = firstQuiz.questions[0]; + const secondQuestion = firstQuiz.questions[1]; + const thirdQuestion = firstQuiz.questions[2]; + + // First question has audio with timestamps + expect(firstQuestion).toHaveProperty('audioData'); + expect(firstQuestion.audioData.audio.filename).toBe( + 'audio-with-timestamps.mp3' + ); + expect(firstQuestion.audioData.audio.startTimestamp).toBe(1.5); + expect(firstQuestion.audioData.audio.finishTimestamp).toBe(3.8); + expect(firstQuestion.audioData.transcript).toEqual([ + { + character: 'Maria', + text: 'Hello, how are you?' + }, + { + character: 'Tom', + text: "I'm doing well, thank you." + } + ]); + expect(firstQuestion.text).toBe( + '

Quiz 1, question 1 with audio timestamps

' + ); + expect(firstQuestion.distractors.length).toBe(3); + expect(firstQuestion.answer).toBe('

Quiz 1, question 1, answer

'); + + // Second question has audio without timestamps + expect(secondQuestion).toHaveProperty('audioData'); + expect(secondQuestion.audioData.audio.filename).toBe('audio-full-file.mp3'); + expect(secondQuestion.audioData.audio.startTimestamp).toBeUndefined(); + expect(secondQuestion.audioData.audio.finishTimestamp).toBeUndefined(); + expect(secondQuestion.audioData.transcript).toEqual([ + { + character: 'Speaker', + text: 'This is the full audio transcript.' + } + ]); + expect(secondQuestion.text).toBe( + '

Quiz 1, question 2 with audio but no timestamps

' + ); + + // Third question has no audio + expect(thirdQuestion.audioData).toBeUndefined(); + expect(thirdQuestion.text).toBe('

Quiz 1, question 3 without audio

'); + }); + + it('should throw error for audio section not in JSON code block', () => { + expect(() => plugin(quizzesWithAudioNotJsonCodeBlockAST, file)).toThrow( + '--audio-- section must contain a ```json code block' + ); + }); + + it('should throw error for invalid JSON in audio section', () => { + expect(() => plugin(quizzesWithAudioInvalidJsonAST, file)).toThrow( + '--audio-- section must contain valid JSON' + ); + }); + + it('should throw error for missing audio filename', () => { + expect(() => plugin(quizzesWithAudioMissingFilenameAST, file)).toThrow( + '--audio-- section must contain audio.filename' + ); + }); + + it('should throw error for transcript not being an array', () => { + expect(() => plugin(quizzesWithAudioTranscriptNotArrayAST, file)).toThrow( + '--audio-- section must contain transcript as an array' + ); + }); + + it('should throw error for empty transcript array', () => { + expect(() => plugin(quizzesWithAudioEmptyTranscriptAST, file)).toThrow( + '--audio-- section transcript array cannot be empty' + ); + }); + + it('should throw error for missing transcript', () => { + expect(() => plugin(quizzesWithAudioMissingTranscriptAST, file)).toThrow( + '--audio-- section must contain transcript as an array' + ); + }); + + it('should throw error for transcript line missing character', () => { + expect(() => + plugin(quizzesWithAudioTranscriptMissingCharacterAST, file) + ).toThrow( + '--audio-- transcript line 0 must have character and text properties' + ); + }); + + it('should throw error for transcript line missing text', () => { + expect(() => + plugin(quizzesWithAudioTranscriptMissingTextAST, file) + ).toThrow( + '--audio-- transcript line 0 must have character and text properties' + ); + }); }); diff --git a/tools/challenge-parser/parser/plugins/validate-sections.js b/tools/challenge-parser/parser/plugins/validate-sections.js index d623d0cc064..c1f39f93f58 100644 --- a/tools/challenge-parser/parser/plugins/validate-sections.js +++ b/tools/challenge-parser/parser/plugins/validate-sections.js @@ -41,6 +41,7 @@ const VALID_MARKERS = [ // Level 4 '#### --answer--', + '#### --audio--', '#### --distractors--', '#### --text--' ]; diff --git a/tools/client-plugins/gatsby-source-challenges/gatsby-node.js b/tools/client-plugins/gatsby-source-challenges/gatsby-node.js index a4722725ef5..0f41169fe86 100644 --- a/tools/client-plugins/gatsby-source-challenges/gatsby-node.js +++ b/tools/client-plugins/gatsby-source-challenges/gatsby-node.js @@ -26,6 +26,20 @@ const idToFilepath = new Map(); // recently overwritten files const idToOverwrittenFile = new Map(); +exports.createSchemaCustomization = ({ actions }) => { + const { createTypes } = actions; + const typeDefs = ` + type QuizQuestion { + text: String! + distractors: [String!]! + answer: String! + audioId: String + transcript: String + } + `; + createTypes(typeDefs); +}; + exports.sourceNodes = function sourceChallengesSourceNodes( { actions, reporter, createNodeId, createContentDigest }, pluginOptions