All files / src/components/QuizEditor QuizEditor.tsx

100% Statements 30/30
100% Branches 4/4
100% Functions 15/15
100% Lines 26/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 12310x   10x 10x 10x   10x 10x                         56x         10x 56x   7x 1x         7x 1x   7x 1x               7x 1x   7x 1x       4x                 7x             28x                       1x                     1x               1x                   1x                      
import update from 'immutability-helper'
 
import { ReactComponent as AddIcon } from '../../assets/svg/add.svg'
import { ReactComponent as CheckedIcon } from '../../assets/svg/checked.svg'
import { ReactComponent as DeleteIcon } from '../../assets/svg/delete.svg'
import { ReactNode } from 'react'
import { Choice, Question, createChoice } from 'sqc-core-functions'
import { Editor } from '../Editor'
 
export type QuizEditorParams = {
  /** The quiz question object */
  question: Question
  /** Whether richtext mode is enabled */
  richtextMode?: boolean
  /** Function to handle any changes on the question, including adding/editing/removing answers */
  onChange: (question: Question) => void
  /** Warning message component to display */
  warning?: ReactNode
}
 
const isAnswerTogglable = (answer: Choice) => answer.isCorrect
 
/**
 * Renders a QuizEditor component with question, answers, and warning.
 */
export const QuizEditor = ({ question, richtextMode, onChange, warning }: QuizEditorParams) => {
  const isAnswerNonRemovable = (answer: Choice) => question.choices.length < 2 || answer.isCorrect
 
  const handleQuestionUpdate = (value: string) => {
    onChange({
      ...question,
      question: value,
    })
  }
  const handleAnswerUpdate = (index: number, value: string) => {
    onChange(update(question, { choices: { [index]: { answer: { $set: value } } } }))
  }
  const handleAddAnswer = (index: number) => {
    onChange(
      update(question, {
        choices: {
          $splice: [[index + 1, 0, createChoice()]],
        },
      })
    )
  }
  const handleRemoveAnswer = (index: number) => {
    onChange(update(question, { choices: { $splice: [[index, 1]] } }))
  }
  const handleToggleAnswer = (choice: Choice) => {
    onChange(
      update(question, {
        choices: {
          $apply: (x: Choice[]): Choice[] =>
            x.map((c) => ({
              ...c,
              isCorrect: c.id === choice.id,
            })),
        },
      })
    )
  }
 
  return (
    <div key={question.id}>
      <h2 className='sqc-mb-2 sqc-text-xl sqc-font-semibold'>Question:</h2>
      <Editor value={question.question} onChange={handleQuestionUpdate} richtextMode={richtextMode} />
      {warning}
      <h2 className='sqc-my-2 sqc-text-xl sqc-font-semibold'>Answers:</h2>
      {question.choices.map((choice, index) => (
        <div key={choice.id}>
          <h3 className='sqc-mb-2 sqc-font-semibold sqc-text-l'>
            Answer {index + 1}: {choice.isCorrect && <span className='sqc-text-emerald-500'>Correct</span>}
          </h3>
          <div className='sqc-relative sqc-mb-6'>
            <div className='sqc-absolute sqc-inset-y-0 sqc-left-0 sqc-flex sqc-flex-col sqc-items-center sqc-justify-center sqc-gap-2 sqc-pl-3'>
              <button
                type='button'
                role='toggle-answer'
                aria-disabled={isAnswerTogglable(choice)}
                disabled={isAnswerTogglable(choice)}
                className='sqc-text-slate-400 hover:sqc-rounded-lg hover:sqc-border hover:sqc-bg-emerald-200 disabled:sqc-cursor-not-allowed disabled:sqc-bg-transparent disabled:sqc-text-emerald-400'
                onClick={() => handleToggleAnswer(choice)}
              >
                <CheckedIcon className='sqc-w-8 sqc-h-8' fill='currentColor' />
                <span className='sqc-sr-only'>Mark as correct answer</span>
              </button>
              <button
                type='button'
                role='remove-answer'
                aria-disabled={isAnswerNonRemovable(choice)}
                disabled={isAnswerNonRemovable(choice)}
                className='sqc-text-red-400 hover:sqc-rounded-lg hover:sqc-border hover:sqc-bg-red-200 disabled:sqc-cursor-not-allowed disabled:sqc-bg-transparent'
                onClick={() => handleRemoveAnswer(index)}
              >
                <DeleteIcon className='sqc-w-8 sqc-h-8' fill='currentColor' />
                <span className='sqc-sr-only'>Remove this answer</span>
              </button>
              <button
                type='button'
                role='add-answer'
                onClick={() => handleAddAnswer(index)}
                className='sqc-text-blue-400 hover:sqc-rounded-lg hover:sqc-border hover:sqc-bg-blue-200'
              >
                <AddIcon className='sqc-w-8 sqc-h-8' fill='currentColor' />
                <span className='sqc-sr-only'>Add an answer after</span>
              </button>
            </div>
            <div className='sqc-block sqc-w-full sqc-rounded-lg sqc-border sqc-border-gray-300 sqc-bg-gray-50 sqc-p-2.5 sqc-pl-14 lg:sqc-py-5'>
              <Editor
                value={choice.answer}
                onChange={(val) => handleAnswerUpdate(index, val)}
                richtextMode={richtextMode}
              />
              {warning}
            </div>
          </div>
        </div>
      ))}
    </div>
  )
}