diff --git a/src/components/question-editor/QuestionEditor.constants.tsx b/src/components/question-editor/QuestionEditor.constants.tsx
new file mode 100644
index 000000000..93e60b0a3
--- /dev/null
+++ b/src/components/question-editor/QuestionEditor.constants.tsx
@@ -0,0 +1,23 @@
+import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'
+import NotesIcon from '@mui/icons-material/Notes'
+import RuleIcon from '@mui/icons-material/Rule'
+
+import { SizeEnum } from '~/types'
+
+export const sortQuestions = [
+ {
+ icon: ,
+ title: 'questionPage.questionType.multipleChoice',
+ value: 'multipleChoice'
+ },
+ {
+ icon: ,
+ title: 'questionPage.questionType.openAnswer',
+ value: 'openAnswer'
+ },
+ {
+ icon: ,
+ title: 'questionPage.questionType.oneAnswer',
+ value: 'oneAnswer'
+ }
+]
diff --git a/src/components/question-editor/QuestionEditor.styles.ts b/src/components/question-editor/QuestionEditor.styles.ts
new file mode 100644
index 000000000..6f63edb92
--- /dev/null
+++ b/src/components/question-editor/QuestionEditor.styles.ts
@@ -0,0 +1,66 @@
+import { commonShadow } from '~/styles/app-theme/custom-shadows'
+
+export const styles = {
+ root: {
+ display: 'flex',
+ padding: '16px 24px',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ borderRadius: '6px',
+ boxShadow: commonShadow
+ },
+ questionHeader: {
+ width: '100%',
+ display: 'flex',
+ justifyContent: 'space-between'
+ },
+ group: {
+ width: '100%'
+ },
+ options: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: '16px'
+ },
+ divider: {
+ alignSelf: 'stretch',
+ mb: '24px'
+ },
+ inputItem: {
+ color: 'basic.black',
+ width: '100%',
+ mb: '8px',
+ '.MuiFormControlLabel-label': {
+ width: '100%'
+ }
+ },
+ answer: {
+ width: '100%',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ },
+ iconWrapper: {
+ display: 'flex',
+ padding: '8px',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: '4px',
+ backgroundColor: 'basic.grey'
+ },
+ selectContainer: {
+ '.MuiOutlinedInput-notchedOutline': { border: 0 }
+ },
+ addRadio: (isEmptyAnswer: boolean) => ({
+ display: 'flex',
+ alignItems: 'center',
+ color: 'primary.600',
+ cursor: isEmptyAnswer ? 'auto' : 'pointer',
+ '& label': {
+ mr: '8px'
+ }
+ }),
+ addIcon: (isEmptyAnswer: boolean) => ({
+ color: isEmptyAnswer ? 'primary.300' : 'primary.700'
+ })
+}
diff --git a/src/components/question-editor/QuestionEditor.tsx b/src/components/question-editor/QuestionEditor.tsx
new file mode 100644
index 000000000..82b8be940
--- /dev/null
+++ b/src/components/question-editor/QuestionEditor.tsx
@@ -0,0 +1,194 @@
+import { ChangeEvent, MouseEvent, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import Box from '@mui/material/Box'
+import Divider from '@mui/material/Divider'
+import IconButton from '@mui/material/IconButton'
+import DeleteIcon from '@mui/icons-material/Delete'
+import CloseIcon from '@mui/icons-material/Close'
+import AddIcon from '@mui/icons-material/Add'
+import Checkbox from '@mui/material/Checkbox'
+import FormControlLabel from '@mui/material/FormControlLabel'
+import FormGroup from '@mui/material/FormGroup'
+import InputBase from '@mui/material/InputBase'
+import Radio from '@mui/material/Radio'
+import RadioGroup from '@mui/material/RadioGroup'
+
+import AppTextField from '~/components/app-text-field/AppTextField'
+import AppSelect from '~/components/app-select/AppSelect'
+
+import { styles } from '~/components/question-editor/QuestionEditor.styles'
+import { sortQuestions } from '~/components/question-editor/QuestionEditor.constants'
+import { Answer, SizeEnum, TextFieldVariantEnum } from '~/types'
+
+const QuestionEditor = () => {
+ const [questionType, setQuestionType] = useState(sortQuestions[0])
+ const [question, setQuestion] = useState('')
+ const [answers, setAnswers] = useState([])
+ const [answer, setAnswer] = useState('')
+
+ const { t } = useTranslation()
+
+ const isMultipleChoice = questionType.value === sortQuestions[0].value
+ const isOpenAnswer = questionType.value === sortQuestions[1].value
+ const isSingleChoice = questionType.value === sortQuestions[2].value
+ const isEmptyAnswer = answers[answers.length - 1]?.text === ''
+
+ const setTypeValue = (value: string) => {
+ const questionOption = sortQuestions.find((item) => item.value === value)
+
+ setQuestionType(questionOption ?? sortQuestions[0])
+ }
+
+ const handleQuestion = (event: ChangeEvent) => {
+ setQuestion(event.target.value)
+ }
+
+ const sortOptions = sortQuestions.map(({ icon, title, value }) => ({
+ title: t(title),
+ value,
+ icon
+ }))
+
+ const handleOptionChange = (index: number, checked: boolean) => {
+ const updatedAnswers = [...answers]
+
+ if (isMultipleChoice) {
+ updatedAnswers[index].isCorrect = checked
+ } else if (isSingleChoice) {
+ updatedAnswers.forEach((answer, i) => {
+ answer.isCorrect = i === index
+ })
+ }
+
+ setAnswers(updatedAnswers)
+ }
+
+ const onChangeInput = (
+ event: ChangeEvent,
+ index: number
+ ) => {
+ const currentValue = event.target.value
+ setAnswers((prevAnswers) => {
+ const updatedAnswers = [...prevAnswers]
+ updatedAnswers[index] = {
+ ...updatedAnswers[index],
+ text: currentValue
+ }
+ return updatedAnswers
+ })
+ }
+
+ const addNewOneAnswer = (event: MouseEvent) => {
+ event.preventDefault()
+ if (!isEmptyAnswer) {
+ setAnswers((prev) => [
+ ...prev,
+ {
+ id: answers.length,
+ text: '',
+ isCorrect: false
+ }
+ ])
+ }
+ }
+
+ const deleteRadioButton = (id: number) => {
+ const updatedAnswers = answers.filter((item) => item.id !== id)
+ setAnswers(updatedAnswers)
+ }
+
+ const handleTypeChange = (value: string) => {
+ setAnswers((prevAnswers) =>
+ prevAnswers.map((answer) => ({ ...answer, isCorrect: false }))
+ )
+ setTypeValue(value)
+ }
+
+ const handleAnswer = (event: ChangeEvent) =>
+ setAnswer(event.target.value)
+
+ const options = answers.map((item) => (
+
+ : }
+ label={
+ onChangeInput(e, item.id)}
+ placeholder={t('questionPage.writeYourAnswer')}
+ value={item.text}
+ />
+ }
+ onChange={(_, checked) => handleOptionChange(item.id, checked)}
+ sx={styles.inputItem}
+ value={item.id}
+ />
+ deleteRadioButton(item.id)}>
+
+
+
+ ))
+
+ return (
+
+
+
+ {questionType.icon}
+
+
+
+
+
+
+
+
+
+
+
+
+ {isMultipleChoice && {options}}
+
+ {isSingleChoice && {options}}
+
+ {!isOpenAnswer && (
+
+ : }
+ disabled={isEmptyAnswer}
+ label={t('questionPage.addNewOne')}
+ value={0}
+ />
+
+
+ )}
+
+ {isOpenAnswer && (
+
+ )}
+
+ )
+}
+
+export default QuestionEditor
diff --git a/src/constants/translations/en/index.ts b/src/constants/translations/en/index.ts
index dcb185a35..f11c209f8 100644
--- a/src/constants/translations/en/index.ts
+++ b/src/constants/translations/en/index.ts
@@ -34,6 +34,7 @@ import myOffersPage from './my-offers-page.json'
import myResourcesPage from './my-resources-page.json'
import chatPage from './chat.json'
import lesson from './lesson.json'
+import questionPage from './question-page.json'
const en = {
translations: {
@@ -72,7 +73,8 @@ const en = {
myOffersPage,
myResourcesPage,
chatPage,
- lesson
+ lesson,
+ questionPage
}
}
diff --git a/src/constants/translations/en/question-page.json b/src/constants/translations/en/question-page.json
new file mode 100644
index 000000000..d6176a65b
--- /dev/null
+++ b/src/constants/translations/en/question-page.json
@@ -0,0 +1,11 @@
+{
+ "question": "Question",
+ "answer": "Answer",
+ "questionType": {
+ "multipleChoice": "Multiple Choice",
+ "openAnswer": "Open Answer",
+ "oneAnswer": "One Answer"
+ },
+ "addNewOne": "Add new one",
+ "writeYourAnswer": "Write your answer"
+}
diff --git a/src/constants/translations/ua/index.ts b/src/constants/translations/ua/index.ts
index d467cb985..3a99447c7 100644
--- a/src/constants/translations/ua/index.ts
+++ b/src/constants/translations/ua/index.ts
@@ -21,6 +21,7 @@ import myOffersPage from './my-offers-page.json'
import myResourcesPage from './my-resources-page.json'
import chatPage from './chat.json'
import lesson from './lesson.json'
+import questionPage from './question-page.json'
const ua = {
translations: {
@@ -46,7 +47,8 @@ const ua = {
myOffersPage,
myResourcesPage,
chatPage,
- lesson
+ lesson,
+ questionPage
}
}
diff --git a/src/constants/translations/ua/question-page.json b/src/constants/translations/ua/question-page.json
new file mode 100644
index 000000000..83326ef92
--- /dev/null
+++ b/src/constants/translations/ua/question-page.json
@@ -0,0 +1,11 @@
+{
+ "question": "Питання",
+ "answer": "Відповідь",
+ "questionType": {
+ "multipleChoice": "Множинний вибір",
+ "openAnswer": "Відкрита відповідь",
+ "oneAnswer": "Одна відповідь"
+ },
+ "addNewOne": "Добавити ще одне питання",
+ "writeYourAnswer": "Напишіть свою відповідь"
+}
diff --git a/src/types/common/enums/common.enums.ts b/src/types/common/enums/common.enums.ts
index bc91fe6b0..338c241d4 100644
--- a/src/types/common/enums/common.enums.ts
+++ b/src/types/common/enums/common.enums.ts
@@ -40,7 +40,8 @@ export enum TypographyVariantEnum {
}
export enum TextFieldVariantEnum {
- Standard = 'standard'
+ Standard = 'standard',
+ Outlined = 'outlined'
}
export enum DrawerVariantEnum {
diff --git a/src/types/index.ts b/src/types/index.ts
index 2e6f8b21e..51fb6bd75 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -22,3 +22,4 @@ export * from '~/types/attachment/attachment.index'
export * from '~/types/my-attachments/myAttachments.index'
export * from '~/types/quizzes/quizzes.index'
export * from '~/types/lesson/lesson.index'
+export * from '~/types/questions/questions.index'
diff --git a/src/types/questions/interfaces/questions.interface.ts b/src/types/questions/interfaces/questions.interface.ts
new file mode 100644
index 000000000..88fd4dc06
--- /dev/null
+++ b/src/types/questions/interfaces/questions.interface.ts
@@ -0,0 +1,5 @@
+export interface Answer {
+ id: number
+ text: string
+ isCorrect: boolean
+}
diff --git a/src/types/questions/questions.index.ts b/src/types/questions/questions.index.ts
new file mode 100644
index 000000000..83e2b538a
--- /dev/null
+++ b/src/types/questions/questions.index.ts
@@ -0,0 +1 @@
+export * from '~/types/questions/interfaces/questions.interface'
diff --git a/src/types/quizzes/interfaces/quizzes.interface.ts b/src/types/quizzes/interfaces/quizzes.interface.ts
index fe2eea307..94d01d8fc 100644
--- a/src/types/quizzes/interfaces/quizzes.interface.ts
+++ b/src/types/quizzes/interfaces/quizzes.interface.ts
@@ -1,14 +1,14 @@
-import { RequestParams, CommonEntityFields, UserResponse } from '~/types'
+import {
+ RequestParams,
+ CommonEntityFields,
+ UserResponse,
+ Answer
+} from '~/types'
export interface QuizzesParams extends RequestParams {
title: string
}
-export interface Answer {
- text: string
- isCorrect: boolean
-}
-
export interface QuestionWithAnswers {
question: string
answers: Answer[]
diff --git a/tests/unit/components/question-editor/QuestionEditor.spec.jsx b/tests/unit/components/question-editor/QuestionEditor.spec.jsx
new file mode 100644
index 000000000..9e5c0801a
--- /dev/null
+++ b/tests/unit/components/question-editor/QuestionEditor.spec.jsx
@@ -0,0 +1,154 @@
+import QuestionEditor from '~/components/question-editor/QuestionEditor'
+import { screen, fireEvent } from '@testing-library/react'
+import { renderWithProviders } from '~tests/test-utils'
+import { beforeEach, describe } from 'vitest'
+
+describe('QuestionEditor component', () => {
+ beforeEach(() => {
+ renderWithProviders()
+ })
+
+ it('should renders question input field', () => {
+ const questionInput = screen.getByLabelText('questionPage.question')
+ expect(questionInput).toBeInTheDocument()
+ })
+
+ it('should renders a open answer', () => {
+ const appSelect = screen.getByTestId('app-select')
+
+ fireEvent.click(appSelect)
+ fireEvent.change(appSelect, {
+ target: { value: 'openAnswer' }
+ })
+
+ const multipleChoiceRadio = screen.getByText(
+ 'questionPage.questionType.openAnswer'
+ )
+ expect(multipleChoiceRadio).toBeInTheDocument()
+ })
+
+ it('should renders a radio buttons for one answer', () => {
+ const appSelect = screen.getByTestId('app-select')
+
+ fireEvent.click(appSelect)
+ fireEvent.change(appSelect, {
+ target: { value: 'oneAnswer' }
+ })
+
+ const multipleChoiceRadio = screen.getByText(
+ 'questionPage.questionType.oneAnswer'
+ )
+ expect(multipleChoiceRadio).toBeInTheDocument()
+ })
+
+ it('should add a new one answer', () => {
+ const addNewOne = screen.getByText('questionPage.addNewOne')
+
+ fireEvent.click(addNewOne)
+ const answer = screen.getByPlaceholderText('questionPage.writeYourAnswer')
+
+ fireEvent.change(answer, {
+ target: { value: 'Changed answer' }
+ })
+
+ const inputValue = screen.getByDisplayValue('Changed answer')
+
+ expect(inputValue).toBeInTheDocument()
+ })
+
+ it('should change question and answer input fields', () => {
+ const appSelect = screen.getByTestId('app-select')
+
+ fireEvent.click(appSelect)
+ fireEvent.change(appSelect, {
+ target: { value: 'openAnswer' }
+ })
+
+ const questionInput = screen.getByLabelText('questionPage.question')
+ const answerInput = screen.getByLabelText('questionPage.answer')
+
+ fireEvent.change(questionInput, {
+ target: { value: 'New question' }
+ })
+
+ fireEvent.change(answerInput, {
+ target: { value: 'New answer' }
+ })
+
+ const questionValue = screen.getByDisplayValue('New question')
+ const answerValue = screen.getByDisplayValue('New answer')
+
+ expect(questionValue).toBeInTheDocument()
+ expect(answerValue).toBeInTheDocument()
+ })
+
+ it('should update answer.isCorrect for single choice questions', () => {
+ const appSelect = screen.getByTestId('app-select')
+ fireEvent.click(appSelect)
+ fireEvent.change(appSelect, {
+ target: { value: 'singleChoice' }
+ })
+
+ const addNewOne = screen.getByText('questionPage.addNewOne')
+ fireEvent.click(addNewOne)
+
+ const firstAnswerCheckbox = screen.getByRole('checkbox', { name: '' })
+
+ fireEvent.click(firstAnswerCheckbox)
+
+ expect(firstAnswerCheckbox).toBeChecked()
+
+ const otherAnswerCheckboxes = screen.getAllByRole('checkbox', { name: '' })
+ otherAnswerCheckboxes.forEach((checkbox, index) => {
+ if (index !== 0) {
+ expect(checkbox).not.toBeChecked()
+ }
+ })
+ })
+
+ it('should update answer.isCorrect for single choice questions', () => {
+ const appSelect = screen.getByTestId('app-select')
+ fireEvent.click(appSelect)
+ fireEvent.change(appSelect, {
+ target: { value: 'oneAnswer' }
+ })
+
+ const addNewOne = screen.getByText('questionPage.addNewOne')
+ fireEvent.click(addNewOne)
+
+ const answer = screen.getByPlaceholderText('questionPage.writeYourAnswer')
+
+ fireEvent.change(answer, {
+ target: { value: 'New answer' }
+ })
+ fireEvent.click(addNewOne)
+
+ const firstAnswerRadio = screen.getAllByRole('radio')[0]
+ const secondAnswerRadio = screen.getAllByRole('radio')[1]
+
+ fireEvent.click(firstAnswerRadio)
+ fireEvent.click(secondAnswerRadio)
+
+ expect(firstAnswerRadio).not.toBeChecked()
+ expect(secondAnswerRadio).toBeChecked()
+ })
+
+ it('should delete a radio button', () => {
+ const addNewOne = screen.getByText('questionPage.addNewOne')
+ fireEvent.click(addNewOne)
+
+ const answer = screen.getByPlaceholderText('questionPage.writeYourAnswer')
+
+ fireEvent.change(answer, {
+ target: { value: 'New answer' }
+ })
+
+ fireEvent.click(addNewOne)
+ const deleteButtons = screen.getAllByTestId('CloseIcon')
+
+ fireEvent.click(deleteButtons[0])
+
+ const remainingAnswers = screen.getAllByRole('checkbox')
+ expect(remainingAnswers.length - 1).toBe(1)
+ })
+})