Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const questionTypeIconMap: Record<Exclude<QuizQuestionType, 'single_choice' | 'i
scale: 'quizRange',
pin_image: 'quizPin',
coordinates: 'quizGraph',
puzzle: 'quizImageMatching',
h5p: 'quizH5p',
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ const questionTypes = {
label: __('Pin', 'tutor'),
icon: 'quizPin',
},
puzzle: {
label: __('Puzzle', 'tutor'),
icon: 'quizImageMatching',
},
h5p: {
label: __('H5P', 'tutor'),
icon: 'quizTrueFalse',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import DrawImage from '@CourseBuilderComponents/curriculum/question-types/DrawIm
import Scale from '@CourseBuilderComponents/curriculum/question-types/Scale';
import PinImage from '@CourseBuilderComponents/curriculum/question-types/PinImage';
import Coordinates from '@CourseBuilderComponents/curriculum/question-types/Coordinates';
import Puzzle from '@CourseBuilderComponents/curriculum/question-types/Puzzle';
import { useQuizModalContext } from '@CourseBuilderContexts/QuizModalContext';

import { tutorConfig } from '@TutorShared/config/config';
Expand Down Expand Up @@ -62,6 +63,7 @@ const QuestionForm = () => {
scale: <Scale key={activeQuestionId} />,
pin_image: <PinImage key={activeQuestionId} />,
coordinates: <Coordinates key={activeQuestionId} />,
puzzle: <Puzzle key={activeQuestionId} />,
} as const;

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ const questionTypeOptions: {
icon: 'quizGraph',
isPro: true,
},
{
label: __('Puzzle', 'tutor'),
value: 'puzzle',
icon: 'quizImageMatching',
isPro: true,
},
];

const isTutorPro = !!tutorConfig.tutor_pro_url;
Expand All @@ -139,7 +145,8 @@ const QuestionList = ({ isEditing }: { isEditing: boolean }) => {
option.value !== 'draw_image' &&
option.value !== 'pin_image' &&
option.value !== 'scale' &&
option.value !== 'coordinates',
option.value !== 'coordinates' &&
option.value !== 'puzzle',
);
}
return questionTypeOptions;
Expand Down Expand Up @@ -289,7 +296,22 @@ const QuestionList = ({ isEditing }: { isEditing: boolean }) => {
is_correct: '1',
},
]
: [],
: questionType === 'puzzle'
? [
{
_data_status: QuizDataStatus.NEW,
is_saved: true,
answer_id: nanoid(),
answer_title: '',
belongs_question_id: questionId,
belongs_question_type: 'puzzle',
answer_two_gap_match: '',
answer_view_format: 'puzzle',
answer_order: 0,
is_correct: '1',
},
]
: [],
answer_explanation: '',
question_mark: 1,
question_order: questionFields.length + 1,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { css } from '@emotion/react';
import { __ } from '@wordpress/i18n';
import { useEffect, useMemo } from 'react';
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';

import { useQuizModalContext } from '@CourseBuilderContexts/QuizModalContext';
import type { QuizForm } from '@CourseBuilderServices/quiz';
import FormSelectInput from '@TutorShared/components/fields/FormSelectInput';
import FormPuzzle from '@TutorShared/components/fields/quiz/questions/FormPuzzle';
import { spacing } from '@TutorShared/config/styles';
import { calculateQuizDataStatus } from '@TutorShared/utils/quiz';
import { styleUtils } from '@TutorShared/utils/style-utils';
import { QuizDataStatus, type QuizQuestionOption } from '@TutorShared/utils/types';
import { nanoid } from '@TutorShared/utils/util';

const Puzzle = () => {
const form = useFormContext<QuizForm>();
const { activeQuestionId, activeQuestionIndex, validationError, setValidationError } = useQuizModalContext();
const activeQuestionDataStatus =
form.watch(`questions.${activeQuestionIndex}._data_status`) ?? QuizDataStatus.NO_CHANGE;

const answersPath = `questions.${activeQuestionIndex}.question_answers` as 'questions.0.question_answers';
const gridSizePath =
`questions.${activeQuestionIndex}.question_settings.puzzle_grid_size` as 'questions.0.question_settings.puzzle_grid_size';

const { fields: optionsFields } = useFieldArray({
control: form.control,
name: answersPath,
});

const gridSizeOptions = useMemo(
() =>
[2, 3, 4, 5, 6, 7].map((value) => ({
label: `${value} x ${value}`,
value,
})),
[],
);

useEffect(() => {
if (!activeQuestionId) {
return;
}
if (optionsFields.length > 0) {
return;
}
const baseAnswer: QuizQuestionOption = {
Comment thread
saadman30 marked this conversation as resolved.
Outdated
_data_status: QuizDataStatus.NEW,
is_saved: true,
answer_id: nanoid(),
belongs_question_id: activeQuestionId,
belongs_question_type: 'puzzle' as QuizQuestionOption['belongs_question_type'],
answer_title: '',
is_correct: '1',
image_id: undefined,
image_url: '',
answer_two_gap_match: '',
answer_view_format: 'puzzle',
answer_order: 0,
};
form.setValue(answersPath, [baseAnswer]);
}, [activeQuestionId, optionsFields.length, answersPath, form]);

useEffect(() => {
const currentValue = form.getValues(gridSizePath);
if (currentValue === undefined || currentValue === null || Number.isNaN(Number(currentValue))) {
form.setValue(gridSizePath, 4);
}
}, [form, gridSizePath]);

if (optionsFields.length === 0) {
return null;
}

return (
<div css={styles.optionWrapper}>
<Controller
key={optionsFields[0]?.id}
control={form.control}
name={`questions.${activeQuestionIndex}.question_answers.0` as 'questions.0.question_answers.0'}
render={(answerControllerProps) => (
<Controller
control={form.control}
name={gridSizePath}
render={(gridSizeControllerProps) => (
<FormPuzzle
{...answerControllerProps}
questionId={activeQuestionId}
validationError={validationError}
setValidationError={setValidationError}
gridSizeControl={
Comment thread
saadman30 marked this conversation as resolved.
Outdated
<FormSelectInput
{...gridSizeControllerProps}
label={__('Grid Size', 'tutor')}
options={gridSizeOptions}
helpText={__('Choose puzzle density from 2 x 2 to 7 x 7.', 'tutor')}
onChange={(option) => {
gridSizeControllerProps.field.onChange(option.value);
if (calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE)) {
form.setValue(
`questions.${activeQuestionIndex}._data_status`,
calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE) as QuizDataStatus,
);
}
}}
/>
}
/>
)}
/>
)}
/>
</div>
);
};

export default Puzzle;

const styles = {
optionWrapper: css`
${styleUtils.display.flex('column')};
gap: ${spacing[16]};
padding-left: ${spacing[40]};
`,
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import MultipleChoicePreview from './question-previews/MultipleChoicePreview';
import OpenEndedPreview from './question-previews/OpenEndedPreview';
import OrderingPreview from './question-previews/OrderingPreview';
import PinImagePreview from './question-previews/PinImagePreview';
import PuzzlePreview from './question-previews/PuzzlePreview';
import ScalePreview from './question-previews/ScalePreview';
import TrueFalsePreview from './question-previews/TrueFalsePreview';
import UnsupportedPreview from './question-previews/UnsupportedPreview';
Expand Down Expand Up @@ -336,6 +337,10 @@ const renderQuestionPreview = (question: QuizQuestion) => {
return <ScalePreview answers={question.question_answers} />;
case 'coordinates':
return <CoordinatesPreview />;
case 'puzzle':
return (
<PuzzlePreview answers={question.question_answers} gridSize={question.question_settings.puzzle_grid_size} />
);
default:
return <UnsupportedPreview />;
}
Expand Down Expand Up @@ -387,6 +392,98 @@ const getPreviewFrameStyles = () => `
box-shadow: none;
}

.tutor-preview-stage .quiz-question-ans-choice-area.tutor-puzzle-question {
Comment thread
saadman30 marked this conversation as resolved.
Outdated
display: flex;
flex-direction: column;
gap: 24px;
width: 100%;
min-width: 0;
margin-top: 40px;
align-items: stretch;
flex-wrap: nowrap;
}

.tutor-preview-stage .tutor-puzzle-question .tutor-puzzle-playground {
position: relative !important;
width: 100%;
aspect-ratio: 1 / 1;
border: 1px solid var(--tutor-border-idle, #d0d5dd);
border-radius: 6px;
overflow: hidden !important;
touch-action: none !important;
background: var(--tutor-surface-l1, #ffffff);
box-sizing: border-box;
}

.tutor-preview-stage .tutor-puzzle-question .tutor-puzzle-reference-image {
display: block;
width: 100%;
height: 100%;
object-fit: fill !important;
pointer-events: none;
}

.tutor-preview-stage .tutor-puzzle-question .tutor-puzzle-slots {
position: absolute;
inset: 0;
pointer-events: none !important;
}

.tutor-preview-stage .tutor-puzzle-question .tutor-puzzle-scatter {
display: flex !important;
flex-wrap: wrap !important;
align-items: flex-start !important;
align-content: flex-start !important;
margin-top: 16px !important;
column-gap: 12px !important;
row-gap: 12px !important;
padding: 12px !important;
border: 2px dashed var(--tutor-border-idle, #d0d5dd) !important;
border-radius: 8px !important;
min-height: 150px !important;
background: var(--tutor-surface-l1, #ffffff) !important;
box-sizing: border-box !important;
width: 100% !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}

.tutor-preview-stage .tutor-puzzle-question .tutor-puzzle-piece {
position: relative;
flex: 0 0 auto !important;
width: auto;
max-width: none;
box-sizing: border-box;
overflow: visible;
border: none;
border-radius: 0;
background: transparent;
background-repeat: no-repeat;
background-position: center;
touch-action: none;
z-index: 2;
transform-origin: center;
transition:
transform 0.2s ease-out,
width 0.2s ease-out,
box-shadow 0.2s ease-out;
backface-visibility: hidden;
box-shadow: none;
}

.tutor-preview-stage .tutor-puzzle-question .tutor-puzzle-piece.is-small {
z-index: 2;
}

.tutor-preview-stage .tutor-puzzle-question .tutor-puzzle-piece--preview-static {
cursor: default !important;
pointer-events: none !important;
border: none !important;
border-radius: 0 !important;
box-shadow: none !important;
background: transparent !important;
}

body[data-preview-device='mobile'] .tutor-draw-image-question .tutor-draw-image-wrapper,
body[data-preview-device='mobile'] .tutor-draw-image-question .tutor-draw-image-reference-inner,
body[data-preview-device='mobile'] .tutor-pin-image-question .tutor-pin-image-wrapper,
Expand Down
Loading
Loading