Skip to content

Commit

Permalink
Save single question with no extra choice
Browse files Browse the repository at this point in the history
  • Loading branch information
leung018 committed Nov 10, 2023
1 parent 5ada66d commit 997de00
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 101 deletions.
30 changes: 23 additions & 7 deletions src/app/components/mc/editor.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { fireEvent, render } from '@testing-library/react'
import { QuestionSetRepoFactory } from '../../../repo/question_set'
import { QuestionSetEditorUIService } from './editor'
import { MultipleChoice } from '../../../model/mc'

describe('QuestionSetEditor', () => {
it('should save question set', () => {
it('should save question set successfully when no extra choice or question added', () => {
const editorRepo = QuestionSetRepoFactory.createTestInstance()
const { getByLabelText, getByText } = render(
QuestionSetEditorUIService.createTestInstance({
Expand All @@ -14,27 +15,42 @@ describe('QuestionSetEditor', () => {
const questionInput = getByLabelText('Question 1:')

const choice1Input = getByLabelText('Choice 1:')
const isChoice1CorrectInput = getByLabelText('Choice 1 is correct answer')
const isChoice1FixedPositionInput = getByLabelText(
'Choice 1 is fixed position',
)

const choice2Input = getByLabelText('Choice 2:')
const isChoice2CorrectInput = getByLabelText('Choice 2 is correct answer')

fireEvent.change(questionSetNameInput, { target: { value: 'Test name' } })
fireEvent.change(questionInput, { target: { value: 'Am I handsome?' } })
fireEvent.change(choice1Input, { target: { value: 'True' } })
fireEvent.change(isChoice1CorrectInput, { target: { value: 'true' } })
fireEvent.change(isChoice1FixedPositionInput, {
target: { value: 'true' },
})
fireEvent.click(isChoice1FixedPositionInput)

fireEvent.change(choice2Input, { target: { value: 'False' } })
fireEvent.click(isChoice2CorrectInput)

fireEvent.click(getByText('Save'))

const actualQuestionSet = editorRepo.getQuestionSetByName('Test name')

// TODO: Assert the question set is saved with expected values
expect(actualQuestionSet.name).toBe('Test name')
expect(actualQuestionSet.questions.length).toBe(1)
expect(actualQuestionSet.questions[0].title).toBe('Am I handsome?')
expect(actualQuestionSet.questions[0].mc).toEqual(
new MultipleChoice({
choices: [
{
answer: 'True',
isFixedPosition: true,
},
{
answer: 'False',
isFixedPosition: false,
},
],
correctChoiceIndex: 1,
}),
)
})
})
229 changes: 139 additions & 90 deletions src/app/components/mc/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
'use client'

import { useState } from 'react'
import {
QuestionSetRepo,
QuestionSetRepoFactory,
} from '../../../repo/question_set'
import { QuestionSet } from '../../../model/question_set'
import { MultipleChoiceBuilder } from '../../../model/mc'

export class QuestionSetEditorUIService {
static create() {
Expand All @@ -22,113 +27,157 @@ export class QuestionSetEditorUIService {
this.editorRepo = editorRepo
}

private handleSave = (questionSet: QuestionSet) => {
this.editorRepo.save(questionSet)
}

getElement() {
return <QuestionSetEditor />
return <QuestionSetEditor onSave={this.handleSave} />
}
}

function QuestionSetEditor() {
return (
<div className="container mx-auto p-4">
<div className="form-group">
<label htmlFor="question-set-name">
<h1 className="text-lg font-bold mb-2">Question Set Name:</h1>
</label>
<input
type="text"
id="question-set-name"
className="border border-gray-300 px-2 py-1 w-full"
/>
</div>
function QuestionSetEditor({
onSave,
}: {
onSave: (questionSet: QuestionSet) => void
}) {
const [questionIndexToNumOfChoices, setQuestionIndexToNumOfChoices] =
useState<Map<number, number>>(new Map<number, number>([[0, 2]]))

const renderChoiceInputs = (questionIndex: number, numOfChoices: number) => {
const choiceInputs = []
for (let i = 0; i < numOfChoices; i++) {
// TODO: turn these into one component and use one key only

<div className="mb-8">
<label>
<h2 className="text-lg font-bold mb-2">Question 1:</h2>
choiceInputs.push(
<div key={i + '-' + 1} className="col-span-3">
<label>
Choice {i + 1}:
<input
type="text"
className="border border-gray-300 px-2 py-1 ml-2"
name={`question-${questionIndex}-choice-${i}-answer`}
/>
</label>
</div>,
)
choiceInputs.push(
<div
key={i + '-' + 2}
className="col-span-1 flex items-center justify-center"
>
<input
type="text"
className="border border-gray-300 px-2 py-1 w-full"
type="checkbox"
className="mr-1"
name={`question-${questionIndex}-choice-${i}-is-correct`}
aria-label={`Choice ${i + 1} is correct answer`}
/>
</label>
</div>,
)
choiceInputs.push(
<div
key={i + '-' + 3}
className="col-span-1 flex items-center justify-center"
>
<input
type="checkbox"
className="mr-1"
name={`question-${questionIndex}-choice-${i}-is-fixed-position`}
aria-label={`Choice ${i + 1} is fixed position`}
/>
</div>,
)
}
return choiceInputs
}

const mapFormDataToQuestionSet = (formData: FormData): QuestionSet => {
return {
name: formData.get('question-set-name') as string,
questions: [
{
title: formData.get('question-0-title') as string,
mc: (() => {
const numOfChoices = questionIndexToNumOfChoices.get(0) as number
const mcBuilder = new MultipleChoiceBuilder()
for (let i = 0; i < numOfChoices; i++) {
mcBuilder.appendChoice({
answer: formData.get(`question-0-choice-${i}-answer`) as string,
isFixedPosition:
formData.get(`question-0-choice-${i}-is-fixed-position`) ===
'on',
})
if (formData.get(`question-0-choice-${i}-is-correct`) === 'on') {
mcBuilder.setCorrectChoiceIndex(i)
}
}
return mcBuilder.build()
})(),
},
],
}
}

return (
<div className="container mx-auto p-4">
<form>
<div className="form-group">
<div className="grid grid-cols-5 gap-4">
<div className="col-span-3"></div>
<div className="col-span-1 text-center">Correct Answer</div>
<div className="col-span-1 text-center">Fixed Position</div>
<label>
<h1 className="text-lg font-bold mb-2">Question Set Name:</h1>
<input
type="text"
name="question-set-name"
className="border border-gray-300 px-2 py-1 w-full"
/>
</label>
</div>

<div className="col-span-3">
<label>
Choice 1:
<input
type="text"
className="border border-gray-300 px-2 py-1 ml-2"
/>
</label>
</div>
<div className="col-span-1 flex items-center justify-center">
<input
type="checkbox"
className="mr-1"
aria-label="Choice 1 is correct answer"
/>
</div>
<div className="col-span-1 flex items-center justify-center">
<input
type="checkbox"
className="mr-1"
aria-label="Choice 1 is fixed position"
/>
</div>
{/* TODO: Refactor the below duplication */}
<div className="col-span-3">
<label>
Choice 2:
<input
type="text"
className="border border-gray-300 px-2 py-1 ml-2"
/>
</label>
</div>
<div className="col-span-1 flex items-center justify-center">
<input
type="checkbox"
className="mr-1"
aria-label="Choice 2 is correct answer"
/>
</div>
<div className="col-span-1 flex items-center justify-center">
<input
type="checkbox"
className="mr-1"
aria-label="Choice 2 is fixed position"
/>
</div>
<div className="mb-8">
<label>
<h2 className="text-lg font-bold mb-2">Question 1:</h2>
<input
type="text"
className="border border-gray-300 px-2 py-1 w-full"
name="question-0-title"
/>
</label>
<div className="form-group">
<div className="grid grid-cols-5 gap-4">
<div className="col-span-3"></div>
<div className="col-span-1 text-center">Correct Answer</div>
<div className="col-span-1 text-center">Fixed Position</div>
{renderChoiceInputs(0, 2)}

<div className="col-span-1">
<button
type="button"
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Add Choice
</button>
<div className="col-span-1">
<button
type="button"
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Add Choice
</button>
</div>
</div>
</div>
</div>
</div>
<button
type="button"
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Add Question
</button>

<div className="flex items-center justify-center w-full">
<button
type="button"
className="bg-green-500 text-white px-4 py-2 rounded"
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Save
Add Question
</button>
</div>

<div className="flex items-center justify-center w-full">
<button
type="button"
className="bg-green-500 text-white px-4 py-2 rounded"
onClick={() =>
onSave(mapFormDataToQuestionSet(new FormData(document.forms[0])))
}
>
Save
</button>
</div>
</form>
</div>
)
}
1 change: 1 addition & 0 deletions src/app/mc/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client'
import { QuestionSetEditorUIService } from '../../components/mc/editor'

export default function QuestionSetEditorPage() {
Expand Down
12 changes: 8 additions & 4 deletions src/repo/question_set.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { QuestionSet } from '../model/question_set'

export interface QuestionSetRepo {
save(questionSet: QuestionSet): void
getQuestionSetByName(questionSetName: string): QuestionSet
}

Expand All @@ -11,10 +12,13 @@ export class QuestionSetRepoFactory {
}

class InMemoryQuestionSetRepo implements QuestionSetRepo {
private nameToQuestionSet: { [name: string]: QuestionSet } = {}

save(questionSet: QuestionSet): void {
this.nameToQuestionSet[questionSet.name] = questionSet
}

getQuestionSetByName(questionSetName: string): QuestionSet {
return {
name: 'Sample Question Set',
questions: [],
}
return this.nameToQuestionSet[questionSetName]
}
}

0 comments on commit 997de00

Please sign in to comment.