@tinyweb_dev/oe-exam-sdk 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/ExamPreviewDialog.d.ts +9 -3
- package/dist/components/exams/ExamPreviewDialog.js +23 -5
- package/dist/components/exams/ExamPreviewPartContent.d.ts +2 -1
- package/dist/components/exams/ExamPreviewPartContent.js +3 -2
- package/dist/components/exams/ExamPreviewSidebar.d.ts +3 -1
- package/dist/components/exams/ExamPreviewSidebar.js +7 -2
- package/dist/components/exams/exam-preview.mode.d.ts +4 -0
- package/dist/components/exams/exam-preview.mode.js +5 -0
- package/dist/components/exams/index.d.ts +1 -0
- package/dist/components/exams/index.js +1 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +10 -2
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.d.ts +3 -0
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +285 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +1 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/types.d.ts +30 -0
- package/dist/components/exams/take/utils/answer-transformers.js +37 -6
- package/dist/components/exams/take/utils/question-transformers.d.ts +28 -1
- package/dist/components/exams/take/utils/question-transformers.js +69 -0
- package/dist/components/questions/_shared/config/question-types.config.js +12 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +32 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.js +1 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +16 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.js +1 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -1
- package/dist/components/questions/creator/question-type-registry.js +4 -0
- package/dist/components/questions/question-bank/QuestionEditorRenderer.d.ts +2 -1
- package/dist/components/questions/question-bank/QuestionEditorRenderer.js +16 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +13 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +183 -0
- package/dist/components/questions/types/answer-the-question-group/index.d.ts +5 -0
- package/dist/components/questions/types/answer-the-question-group/index.js +5 -0
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.d.ts +4 -0
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +83 -0
- package/dist/components/questions/types/answer-the-question-group/register.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question-group/register.js +36 -0
- package/dist/components/questions/types/answer-the-question-group/transform.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question-group/transform.js +72 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +11 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +69 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.d.ts +8 -0
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +213 -0
- package/dist/components/questions/types/crossword-puzzle/index.d.ts +4 -0
- package/dist/components/questions/types/crossword-puzzle/index.js +4 -0
- package/dist/components/questions/types/crossword-puzzle/register.d.ts +2 -0
- package/dist/components/questions/types/crossword-puzzle/register.js +29 -0
- package/dist/components/questions/types/crossword-puzzle/transform.d.ts +2 -0
- package/dist/components/questions/types/crossword-puzzle/transform.js +75 -0
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -1
- package/dist/components/questions/viewer/QuestionViewer.js +9 -6
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +4 -4
- package/dist/components/results/ReviewQuestionRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +19 -0
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +27 -0
- package/dist/components/results/renderers/review-question-body-dedicated.d.ts +1 -0
- package/dist/components/results/renderers/review-question-body-dedicated.js +47 -2
- package/dist/components/results/renderers/review-question-body-movers.js +7 -2
- package/dist/components/results/review-data.js +20 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +4 -0
- package/dist/shared/lib/i18n/messages/en/admin.d.ts +5 -0
- package/dist/shared/lib/i18n/messages/en/admin.js +5 -0
- package/dist/shared/lib/i18n/messages/en.d.ts +5 -0
- package/dist/shared/lib/i18n/messages/vi/admin.d.ts +5 -0
- package/dist/shared/lib/i18n/messages/vi/admin.js +5 -0
- package/dist/shared/lib/i18n/messages/vi.d.ts +5 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +93 -0
- package/dist/shared/lib/utils/question-transform-validation.js +12 -0
- package/dist/shared/lib/utils/question-transform.js +4 -0
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/answer-the-question-group.d.ts +39 -0
- package/dist/shared/types/questions/answer-the-question-group.js +1 -0
- package/dist/shared/types/questions/crossword-puzzle.d.ts +50 -0
- package/dist/shared/types/questions/crossword-puzzle.js +10 -0
- package/dist/shared/types/questions/index.d.ts +2 -0
- package/dist/shared/types/questions/index.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const DEFAULT_INSTRUCTION = 'Read the text again and answer the questions.';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return isRecord(value) ? value : {};
|
|
7
|
+
}
|
|
8
|
+
function asString(value, fallback = '') {
|
|
9
|
+
return typeof value === 'string' ? value : fallback;
|
|
10
|
+
}
|
|
11
|
+
function normalizeExpectedAnswers(value) {
|
|
12
|
+
if (!Array.isArray(value)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return value
|
|
16
|
+
.filter((item) => typeof item === 'string')
|
|
17
|
+
.map((item) => item.trim())
|
|
18
|
+
.filter((item) => item !== '');
|
|
19
|
+
}
|
|
20
|
+
export function createEmptyAnswerTheQuestionGroupItem(questionNumber) {
|
|
21
|
+
return {
|
|
22
|
+
question: '',
|
|
23
|
+
expectedAnswers: [''],
|
|
24
|
+
isExample: false,
|
|
25
|
+
points: 1,
|
|
26
|
+
questionNumber,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function mapAnswerTheQuestionGroupItem(item, index, fallbackAnswers) {
|
|
30
|
+
const record = asRecord(item);
|
|
31
|
+
const content = asRecord(record.content);
|
|
32
|
+
const correctAnswer = asRecord(record.correctAnswer);
|
|
33
|
+
const isExample = record.isExample === true;
|
|
34
|
+
const nested = normalizeExpectedAnswers(correctAnswer.expectedAnswers);
|
|
35
|
+
const expectedAnswers = nested.length > 0
|
|
36
|
+
? nested
|
|
37
|
+
: (normalizeExpectedAnswers(fallbackAnswers).length > 0
|
|
38
|
+
? normalizeExpectedAnswers(fallbackAnswers)
|
|
39
|
+
: ['']);
|
|
40
|
+
return {
|
|
41
|
+
question: asString(content.question) || asString(record.question),
|
|
42
|
+
expectedAnswers,
|
|
43
|
+
isExample,
|
|
44
|
+
points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
|
|
45
|
+
questionNumber: typeof record.questionNumber === 'number' ? record.questionNumber : index + 1,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function mapQuestionToAnswerTheQuestionGroupData(question) {
|
|
49
|
+
const source = asRecord(question);
|
|
50
|
+
const content = asRecord(source.content);
|
|
51
|
+
const answer = asRecord(source.answer);
|
|
52
|
+
const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
|
|
53
|
+
const explanation = String(source.explanation || answer.explanation || '');
|
|
54
|
+
const fallbackAnswers = Array.isArray(correctAnswer.answer)
|
|
55
|
+
? correctAnswer.answer
|
|
56
|
+
: [];
|
|
57
|
+
if (Array.isArray(content.items)) {
|
|
58
|
+
const items = content.items.map((item, index) => mapAnswerTheQuestionGroupItem(item, index, fallbackAnswers[index]?.expectedAnswers));
|
|
59
|
+
return {
|
|
60
|
+
instruction: asString(asRecord(content.meta).instruction) || DEFAULT_INSTRUCTION,
|
|
61
|
+
passage: asString(asRecord(content.meta).passage),
|
|
62
|
+
items,
|
|
63
|
+
explanation,
|
|
64
|
+
points: items.reduce((total, item) => (item.isExample ? total : total + (item.points || 0)), 0),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(answer.items)) {
|
|
68
|
+
return {
|
|
69
|
+
instruction: asString(answer.instruction, DEFAULT_INSTRUCTION),
|
|
70
|
+
passage: asString(answer.passage),
|
|
71
|
+
items: answer.items,
|
|
72
|
+
explanation,
|
|
73
|
+
points: typeof answer.points === 'number' ? answer.points : 1,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
instruction: DEFAULT_INSTRUCTION,
|
|
78
|
+
passage: '',
|
|
79
|
+
items: [createEmptyAnswerTheQuestionGroupItem(1)],
|
|
80
|
+
explanation,
|
|
81
|
+
points: 1,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AnswerTheQuestionGroupCreator } from './AnswerTheQuestionGroupCreator';
|
|
2
|
+
import { mapQuestionToAnswerTheQuestionGroupData } from './map-answer-the-question-group-data';
|
|
3
|
+
export const answerTheQuestionGroupRegistration = {
|
|
4
|
+
component: AnswerTheQuestionGroupCreator,
|
|
5
|
+
transformInitialData: (initialData) => mapQuestionToAnswerTheQuestionGroupData(initialData),
|
|
6
|
+
getExtraProps: (ctx) => ({
|
|
7
|
+
onUnsavedChangesChange: ctx.onUnsavedChangesChange,
|
|
8
|
+
validationRef: ctx.validationRef,
|
|
9
|
+
}),
|
|
10
|
+
wrapOnSave: (data, ctx) => {
|
|
11
|
+
const payload = data;
|
|
12
|
+
return {
|
|
13
|
+
type: ctx.questionType,
|
|
14
|
+
content: ctx.state.content,
|
|
15
|
+
points: payload.points ?? ctx.state.points ?? 1,
|
|
16
|
+
level: ctx.state.level,
|
|
17
|
+
difficulty: ctx.state.difficulty,
|
|
18
|
+
skill: ctx.state.skill,
|
|
19
|
+
answer: payload,
|
|
20
|
+
explanation: payload.explanation || '',
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
wrapOnChange: (data, ctx) => {
|
|
24
|
+
const payload = data;
|
|
25
|
+
return {
|
|
26
|
+
type: ctx.questionType,
|
|
27
|
+
content: ctx.state.content,
|
|
28
|
+
points: payload.points ?? ctx.state.points ?? 1,
|
|
29
|
+
level: ctx.state.level,
|
|
30
|
+
difficulty: ctx.state.difficulty,
|
|
31
|
+
skill: ctx.state.skill,
|
|
32
|
+
answer: payload,
|
|
33
|
+
explanation: payload.explanation || '',
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const CHILD_QUESTION_TYPE = 'ANSWER_THE_QUESTION';
|
|
2
|
+
const GRADING_TYPE = 'AI';
|
|
3
|
+
const DEFAULT_INSTRUCTION = 'Read the text again and answer the questions.';
|
|
4
|
+
function normalizeExpectedAnswers(value) {
|
|
5
|
+
if (!Array.isArray(value)) {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
return value
|
|
9
|
+
.filter((item) => typeof item === 'string')
|
|
10
|
+
.map((item) => item.trim())
|
|
11
|
+
.filter((item) => item !== '');
|
|
12
|
+
}
|
|
13
|
+
function sumGradablePoints(items) {
|
|
14
|
+
return items.reduce((total, item) => {
|
|
15
|
+
if (item.isExample) {
|
|
16
|
+
return total;
|
|
17
|
+
}
|
|
18
|
+
const points = typeof item.points === 'number' ? item.points : 1;
|
|
19
|
+
return total + points;
|
|
20
|
+
}, 0);
|
|
21
|
+
}
|
|
22
|
+
function isContentEmpty(html) {
|
|
23
|
+
return !html || html === '<p></p>' || html === '<p><br></p>' || html.trim() === '';
|
|
24
|
+
}
|
|
25
|
+
export const transformAnswerTheQuestionGroup = (question) => {
|
|
26
|
+
const answerData = question.answer;
|
|
27
|
+
const items = Array.isArray(answerData?.items) ? answerData.items : [];
|
|
28
|
+
if (items.length === 0) {
|
|
29
|
+
return {
|
|
30
|
+
apiContent: {
|
|
31
|
+
meta: {
|
|
32
|
+
instruction: DEFAULT_INSTRUCTION,
|
|
33
|
+
gradingType: GRADING_TYPE,
|
|
34
|
+
isAiGraded: true,
|
|
35
|
+
},
|
|
36
|
+
items: [],
|
|
37
|
+
},
|
|
38
|
+
apiCorrectAnswer: { answer: [] },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const apiItems = items.map((item, index) => {
|
|
42
|
+
const expectedAnswers = normalizeExpectedAnswers(item.expectedAnswers);
|
|
43
|
+
return {
|
|
44
|
+
questionType: CHILD_QUESTION_TYPE,
|
|
45
|
+
...(item.isExample ? { isExample: true } : {}),
|
|
46
|
+
content: {
|
|
47
|
+
question: item.question || '',
|
|
48
|
+
},
|
|
49
|
+
correctAnswer: { expectedAnswers },
|
|
50
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
51
|
+
questionNumber: item.questionNumber || index + 1,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
const passage = answerData?.passage?.trim();
|
|
55
|
+
return {
|
|
56
|
+
apiContent: {
|
|
57
|
+
meta: {
|
|
58
|
+
instruction: answerData?.instruction || DEFAULT_INSTRUCTION,
|
|
59
|
+
...(!isContentEmpty(passage) ? { passage } : {}),
|
|
60
|
+
gradingType: GRADING_TYPE,
|
|
61
|
+
isAiGraded: true,
|
|
62
|
+
},
|
|
63
|
+
items: apiItems,
|
|
64
|
+
},
|
|
65
|
+
apiCorrectAnswer: {
|
|
66
|
+
answer: apiItems.map((item) => ({
|
|
67
|
+
expectedAnswers: item.correctAnswer.expectedAnswers,
|
|
68
|
+
})),
|
|
69
|
+
},
|
|
70
|
+
totalPoints: sumGradablePoints(items),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { CrosswordPuzzleContent, CrosswordPuzzleCorrectAnswer } from '../../../../shared/types/questions/crossword-puzzle';
|
|
3
|
+
interface CrosswordPuzzleClientProps {
|
|
4
|
+
content: CrosswordPuzzleContent;
|
|
5
|
+
correctAnswer?: CrosswordPuzzleCorrectAnswer;
|
|
6
|
+
isReviewMode?: boolean;
|
|
7
|
+
userAnswers?: Record<string, string>;
|
|
8
|
+
onAnswerChange?: (key: string, value: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode, userAnswers, }: CrosswordPuzzleClientProps): React.JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = false, userAnswers = {}, }) {
|
|
5
|
+
const rows = content?.gridSize?.rows || 12;
|
|
6
|
+
const cols = content?.gridSize?.cols || 12;
|
|
7
|
+
const acrossClues = content?.clues?.across || [];
|
|
8
|
+
const downClues = content?.clues?.down || [];
|
|
9
|
+
// Build grid cell mapping
|
|
10
|
+
const { cellMap, clueNumberMap } = useMemo(() => {
|
|
11
|
+
const cMap = new Map();
|
|
12
|
+
const numMap = new Map();
|
|
13
|
+
for (const c of acrossClues) {
|
|
14
|
+
numMap.set(`${c.startRow}-${c.startCol}`, c.number);
|
|
15
|
+
for (let i = 0; i < c.length; i++) {
|
|
16
|
+
const key = `${c.startRow}-${c.startCol + i}`;
|
|
17
|
+
const existing = cMap.get(key) || {};
|
|
18
|
+
cMap.set(key, { ...existing, across: c });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const c of downClues) {
|
|
22
|
+
if (!numMap.has(`${c.startRow}-${c.startCol}`)) {
|
|
23
|
+
numMap.set(`${c.startRow}-${c.startCol}`, c.number);
|
|
24
|
+
}
|
|
25
|
+
for (let i = 0; i < c.length; i++) {
|
|
26
|
+
const key = `${c.startRow + i}-${c.startCol}`;
|
|
27
|
+
const existing = cMap.get(key) || {};
|
|
28
|
+
cMap.set(key, { ...existing, down: c });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { cellMap: cMap, clueNumberMap: numMap };
|
|
32
|
+
}, [acrossClues, downClues]);
|
|
33
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-4 bg-white rounded-xl border border-gray-200", children: [content?.title && (_jsx("h3", { className: "text-lg font-bold text-gray-800 text-center", children: content.title })), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-6 items-start", children: [_jsxs("div", { className: "lg:col-span-5 flex flex-col gap-4", children: [_jsxs("div", { className: "rounded-lg border border-blue-200 bg-blue-50/50 p-4", children: [_jsxs("h4", { className: "font-bold text-blue-900 mb-2.5 flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-blue-600 text-white text-xs font-semibold", children: "Across" }), _jsx("span", { children: "H\u00E0ng ngang" })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-60 overflow-y-auto pr-1", children: acrossClues.map((c) => {
|
|
34
|
+
const answer = correctAnswer?.answers?.[`across-${c.number}`];
|
|
35
|
+
const userAns = userAnswers[`across-${c.number}`];
|
|
36
|
+
return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-blue-100", children: [_jsxs("span", { className: "font-bold text-blue-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [_jsx("span", { className: "text-gray-800", children: c.clue }), isReviewMode && answer && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: answer }), userAns && userAns !== answer && (_jsx("span", { className: "ml-2 line-through text-red-500 font-semibold", children: userAns }))] }))] }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", c.length, " ch\u1EEF)"] })] }, `across-${c.number}`));
|
|
37
|
+
}) })] }), _jsxs("div", { className: "rounded-lg border border-purple-200 bg-purple-50/50 p-4", children: [_jsxs("h4", { className: "font-bold text-purple-900 mb-2.5 flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-purple-600 text-white text-xs font-semibold", children: "Down" }), _jsx("span", { children: "H\u00E0ng d\u1ECDc" })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-60 overflow-y-auto pr-1", children: downClues.map((c) => {
|
|
38
|
+
const answer = correctAnswer?.answers?.[`down-${c.number}`];
|
|
39
|
+
const userAns = userAnswers[`down-${c.number}`];
|
|
40
|
+
return (_jsxs("div", { className: "flex items-start gap-2 text-sm bg-white p-2 rounded border border-purple-100", children: [_jsxs("span", { className: "font-bold text-purple-700 min-w-[20px]", children: [c.number, "."] }), _jsxs("div", { className: "flex-1", children: [_jsx("span", { className: "text-gray-800", children: c.clue }), isReviewMode && answer && (_jsxs("div", { className: "text-xs mt-1", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n: " }), _jsx("span", { className: "font-bold text-green-700", children: answer }), userAns && userAns !== answer && (_jsx("span", { className: "ml-2 line-through text-red-500 font-semibold", children: userAns }))] }))] }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", c.length, " ch\u1EEF)"] })] }, `down-${c.number}`));
|
|
41
|
+
}) })] })] }), _jsx("div", { className: "lg:col-span-7 flex justify-center overflow-x-auto p-4 bg-gray-50 rounded-xl border border-gray-200", children: _jsx("div", { className: "inline-grid gap-1", style: {
|
|
42
|
+
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
43
|
+
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
44
|
+
const r = rIdx + 1;
|
|
45
|
+
return Array.from({ length: cols }).map((_, cIdx) => {
|
|
46
|
+
const c = cIdx + 1;
|
|
47
|
+
const key = `${r}-${c}`;
|
|
48
|
+
const cellInfo = cellMap.get(key);
|
|
49
|
+
const clueNum = clueNumberMap.get(key);
|
|
50
|
+
if (!cellInfo) {
|
|
51
|
+
return (_jsx("div", { className: "h-8 w-8 sm:h-9 sm:w-9 md:h-10 md:w-10 bg-transparent" }, key));
|
|
52
|
+
}
|
|
53
|
+
let previewLetter = '';
|
|
54
|
+
if (correctAnswer?.answers) {
|
|
55
|
+
if (cellInfo.across) {
|
|
56
|
+
const word = correctAnswer.answers[`across-${cellInfo.across.number}`] || '';
|
|
57
|
+
const offset = c - cellInfo.across.startCol;
|
|
58
|
+
previewLetter = word[offset] || '';
|
|
59
|
+
}
|
|
60
|
+
else if (cellInfo.down) {
|
|
61
|
+
const word = correctAnswer.answers[`down-${cellInfo.down.number}`] || '';
|
|
62
|
+
const offset = r - cellInfo.down.startRow;
|
|
63
|
+
previewLetter = word[offset] || '';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return (_jsxs("div", { className: "relative flex items-center justify-center rounded border-2 border-gray-400 bg-white font-bold text-gray-800 shadow-sm h-8 w-8 sm:h-9 sm:w-9 md:h-10 md:w-10 text-sm sm:text-base md:text-lg", children: [clueNum && (_jsx("span", { className: "absolute top-0.5 left-1 text-[9px] font-bold text-gray-500 leading-none", children: clueNum })), _jsx("span", { children: previewLetter })] }, key));
|
|
67
|
+
});
|
|
68
|
+
}) }) })] })] }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface CrosswordPuzzleCreatorProps {
|
|
3
|
+
initialData?: any;
|
|
4
|
+
onChange?: (data: any) => void;
|
|
5
|
+
onUnsavedChangesChange?: (hasChanges: boolean) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChangesChange, }: CrosswordPuzzleCreatorProps): React.JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import { Plus, Trash2, Edit2, Check, AlertCircle } from 'lucide-react';
|
|
5
|
+
export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChangesChange, }) {
|
|
6
|
+
const [title, setTitle] = useState(() => {
|
|
7
|
+
return initialData?.content?.title || 'Nationalities Crossword';
|
|
8
|
+
});
|
|
9
|
+
const [rows, setRows] = useState(() => {
|
|
10
|
+
return initialData?.content?.gridSize?.rows || initialData?.content?.rows || 12;
|
|
11
|
+
});
|
|
12
|
+
const [cols, setCols] = useState(() => {
|
|
13
|
+
return initialData?.content?.gridSize?.cols || initialData?.content?.cols || 12;
|
|
14
|
+
});
|
|
15
|
+
const [caseSensitive, setCaseSensitive] = useState(() => {
|
|
16
|
+
return initialData?.answer?.caseSensitive || initialData?.correctAnswer?.caseSensitive || false;
|
|
17
|
+
});
|
|
18
|
+
const [points, setPoints] = useState(() => {
|
|
19
|
+
return initialData?.points ?? 11;
|
|
20
|
+
});
|
|
21
|
+
const [clues, setClues] = useState(() => {
|
|
22
|
+
if (Array.isArray(initialData?.content?.clues)) {
|
|
23
|
+
return initialData.content.clues;
|
|
24
|
+
}
|
|
25
|
+
const across = initialData?.content?.clues?.across || [];
|
|
26
|
+
const down = initialData?.content?.clues?.down || [];
|
|
27
|
+
const answers = initialData?.answer?.answers || initialData?.correctAnswer?.answers || {};
|
|
28
|
+
const items = [];
|
|
29
|
+
for (const a of across) {
|
|
30
|
+
items.push({
|
|
31
|
+
id: `across-${a.number}-${Date.now()}-${Math.random()}`,
|
|
32
|
+
number: a.number,
|
|
33
|
+
clue: a.clue,
|
|
34
|
+
startRow: a.startRow,
|
|
35
|
+
startCol: a.startCol,
|
|
36
|
+
length: a.length,
|
|
37
|
+
direction: 'across',
|
|
38
|
+
answer: answers[`across-${a.number}`] || '',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
for (const d of down) {
|
|
42
|
+
items.push({
|
|
43
|
+
id: `down-${d.number}-${Date.now()}-${Math.random()}`,
|
|
44
|
+
number: d.number,
|
|
45
|
+
clue: d.clue,
|
|
46
|
+
startRow: d.startRow,
|
|
47
|
+
startCol: d.startCol,
|
|
48
|
+
length: d.length,
|
|
49
|
+
direction: 'down',
|
|
50
|
+
answer: answers[`down-${d.number}`] || '',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return items;
|
|
54
|
+
});
|
|
55
|
+
const [editingClueId, setEditingClueId] = useState(null);
|
|
56
|
+
const [formNumber, setFormNumber] = useState(1);
|
|
57
|
+
const [formDirection, setFormDirection] = useState('across');
|
|
58
|
+
const [formClue, setFormClue] = useState('');
|
|
59
|
+
const [formAnswer, setFormAnswer] = useState('');
|
|
60
|
+
const [formStartRow, setFormStartRow] = useState(1);
|
|
61
|
+
const [formStartCol, setFormStartCol] = useState(1);
|
|
62
|
+
const { cellMap, clueNumberMap, conflicts } = useMemo(() => {
|
|
63
|
+
const cMap = new Map();
|
|
64
|
+
const numMap = new Map();
|
|
65
|
+
const conflictList = [];
|
|
66
|
+
for (const item of clues) {
|
|
67
|
+
const startKey = `${item.startRow}-${item.startCol}`;
|
|
68
|
+
const existingNums = numMap.get(startKey) || [];
|
|
69
|
+
if (!existingNums.includes(item.number)) {
|
|
70
|
+
numMap.set(startKey, [...existingNums, item.number]);
|
|
71
|
+
}
|
|
72
|
+
const word = (item.answer || '').trim().toUpperCase();
|
|
73
|
+
const length = item.length || word.length || 1;
|
|
74
|
+
for (let i = 0; i < length; i++) {
|
|
75
|
+
const r = item.direction === 'across' ? item.startRow : item.startRow + i;
|
|
76
|
+
const c = item.direction === 'across' ? item.startCol + i : item.startCol;
|
|
77
|
+
const key = `${r}-${c}`;
|
|
78
|
+
const letter = word[i] || '';
|
|
79
|
+
const list = cMap.get(key) || [];
|
|
80
|
+
if (letter && list.some((x) => x.letter && x.letter !== letter)) {
|
|
81
|
+
conflictList.push(key);
|
|
82
|
+
}
|
|
83
|
+
list.push({ letter, clueId: item.id, dir: item.direction });
|
|
84
|
+
cMap.set(key, list);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { cellMap: cMap, clueNumberMap: numMap, conflicts: conflictList };
|
|
88
|
+
}, [clues]);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!onChange)
|
|
91
|
+
return;
|
|
92
|
+
const across = clues
|
|
93
|
+
.filter((c) => c.direction === 'across')
|
|
94
|
+
.map((c) => ({
|
|
95
|
+
number: c.number,
|
|
96
|
+
clue: c.clue.trim(),
|
|
97
|
+
startRow: c.startRow,
|
|
98
|
+
startCol: c.startCol,
|
|
99
|
+
length: c.length || c.answer.trim().length,
|
|
100
|
+
}));
|
|
101
|
+
const down = clues
|
|
102
|
+
.filter((c) => c.direction === 'down')
|
|
103
|
+
.map((c) => ({
|
|
104
|
+
number: c.number,
|
|
105
|
+
clue: c.clue.trim(),
|
|
106
|
+
startRow: c.startRow,
|
|
107
|
+
startCol: c.startCol,
|
|
108
|
+
length: c.length || c.answer.trim().length,
|
|
109
|
+
}));
|
|
110
|
+
const answers = {};
|
|
111
|
+
for (const c of clues) {
|
|
112
|
+
if (c.answer) {
|
|
113
|
+
answers[`${c.direction}-${c.number}`] = c.answer.trim().toUpperCase();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
onChange({
|
|
117
|
+
content: {
|
|
118
|
+
title,
|
|
119
|
+
gridSize: { rows, cols },
|
|
120
|
+
clues: { across, down },
|
|
121
|
+
},
|
|
122
|
+
answer: {
|
|
123
|
+
answers,
|
|
124
|
+
caseSensitive,
|
|
125
|
+
},
|
|
126
|
+
points,
|
|
127
|
+
});
|
|
128
|
+
onUnsavedChangesChange?.(true);
|
|
129
|
+
}, [title, rows, cols, clues, caseSensitive, points, onChange, onUnsavedChangesChange]);
|
|
130
|
+
const handleSaveClue = (e) => {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
if (!formClue.trim() || !formAnswer.trim())
|
|
133
|
+
return;
|
|
134
|
+
const cleanAnswer = formAnswer.trim().toUpperCase();
|
|
135
|
+
const length = cleanAnswer.length;
|
|
136
|
+
if (editingClueId) {
|
|
137
|
+
setClues((prev) => prev.map((c) => c.id === editingClueId
|
|
138
|
+
? {
|
|
139
|
+
...c,
|
|
140
|
+
number: formNumber,
|
|
141
|
+
direction: formDirection,
|
|
142
|
+
clue: formClue.trim(),
|
|
143
|
+
answer: cleanAnswer,
|
|
144
|
+
startRow: formStartRow,
|
|
145
|
+
startCol: formStartCol,
|
|
146
|
+
length,
|
|
147
|
+
}
|
|
148
|
+
: c));
|
|
149
|
+
setEditingClueId(null);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const newClue = {
|
|
153
|
+
id: `clue-${Date.now()}-${Math.random()}`,
|
|
154
|
+
number: formNumber,
|
|
155
|
+
direction: formDirection,
|
|
156
|
+
clue: formClue.trim(),
|
|
157
|
+
answer: cleanAnswer,
|
|
158
|
+
startRow: formStartRow,
|
|
159
|
+
startCol: formStartCol,
|
|
160
|
+
length,
|
|
161
|
+
};
|
|
162
|
+
setClues((prev) => [...prev, newClue]);
|
|
163
|
+
}
|
|
164
|
+
setFormClue('');
|
|
165
|
+
setFormAnswer('');
|
|
166
|
+
setFormNumber((prev) => prev + 1);
|
|
167
|
+
};
|
|
168
|
+
const handleEditClue = (c) => {
|
|
169
|
+
setEditingClueId(c.id);
|
|
170
|
+
setFormNumber(c.number);
|
|
171
|
+
setFormDirection(c.direction);
|
|
172
|
+
setFormClue(c.clue);
|
|
173
|
+
setFormAnswer(c.answer);
|
|
174
|
+
setFormStartRow(c.startRow);
|
|
175
|
+
setFormStartCol(c.startCol);
|
|
176
|
+
};
|
|
177
|
+
const handleDeleteClue = (id) => {
|
|
178
|
+
setClues((prev) => prev.filter((c) => c.id !== id));
|
|
179
|
+
if (editingClueId === id) {
|
|
180
|
+
setEditingClueId(null);
|
|
181
|
+
setFormClue('');
|
|
182
|
+
setFormAnswer('');
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-4 bg-white rounded-xl border border-gray-200", children: [_jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 p-4 bg-gray-50 rounded-lg border border-gray-200", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-gray-700 mb-1", children: "Ti\u00EAu \u0111\u1EC1 \u00F4 ch\u1EEF" }), _jsx("input", { type: "text", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "vd: Nationalities Crossword", className: "w-full px-3 py-1.5 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 outline-none" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-gray-700 mb-1", children: "K\u00EDch th\u01B0\u1EDBc l\u01B0\u1EDBi (H\u00E0ng x C\u1ED9t)" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "number", min: 3, max: 30, value: rows, onChange: (e) => setRows(Math.max(3, parseInt(e.target.value) || 12)), className: "w-20 px-2 py-1.5 text-sm border border-gray-300 rounded outline-none" }), _jsx("span", { className: "text-gray-500 font-bold", children: "x" }), _jsx("input", { type: "number", min: 3, max: 30, value: cols, onChange: (e) => setCols(Math.max(3, parseInt(e.target.value) || 12)), className: "w-20 px-2 py-1.5 text-sm border border-gray-300 rounded outline-none" })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-gray-700 mb-1", children: "\u0110i\u1EC3m s\u1ED1" }), _jsx("input", { type: "number", min: 0, step: 0.5, value: points, onChange: (e) => setPoints(parseFloat(e.target.value) || 1), className: "w-full px-3 py-1.5 text-sm border border-gray-300 rounded outline-none" })] }), _jsx("div", { className: "flex items-center pt-5", children: _jsxs("label", { className: "flex items-center gap-2 text-sm text-gray-700 cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: caseSensitive, onChange: (e) => setCaseSensitive(e.target.checked), className: "rounded text-blue-600 focus:ring-blue-500 h-4 w-4" }), _jsx("span", { children: "Ph\u00E2n bi\u1EC7t hoa/th\u01B0\u1EDDng" })] }) })] }), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-6", children: [_jsxs("div", { className: "lg:col-span-5 flex flex-col gap-4", children: [_jsxs("form", { onSubmit: handleSaveClue, className: "p-4 rounded-xl border border-blue-200 bg-blue-50/40 flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm font-bold text-blue-900", children: editingClueId ? 'Chỉnh sửa gợi ý' : 'Thêm gợi ý mới' }), editingClueId && (_jsx("button", { type: "button", onClick: () => {
|
|
186
|
+
setEditingClueId(null);
|
|
187
|
+
setFormClue('');
|
|
188
|
+
setFormAnswer('');
|
|
189
|
+
}, className: "text-xs text-gray-500 hover:text-gray-700 underline", children: "H\u1EE7y ch\u1EC9nh s\u1EEDa" }))] }), _jsxs("div", { className: "grid grid-cols-3 gap-2", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[11px] font-semibold text-gray-600 mb-0.5", children: "S\u1ED1 Clue" }), _jsx("input", { type: "number", min: 1, value: formNumber, onChange: (e) => setFormNumber(parseInt(e.target.value) || 1), className: "w-full px-2 py-1 text-sm border border-gray-300 rounded outline-none" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[11px] font-semibold text-gray-600 mb-0.5", children: "H\u01B0\u1EDBng" }), _jsxs("select", { value: formDirection, onChange: (e) => setFormDirection(e.target.value), className: "w-full px-2 py-1 text-sm border border-gray-300 rounded outline-none bg-white", children: [_jsx("option", { value: "across", children: "Across (Ngang)" }), _jsx("option", { value: "down", children: "Down (D\u1ECDc)" })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[11px] font-semibold text-gray-600 mb-0.5", children: "To\u1EA1 \u0111\u1ED9 (R, C)" }), _jsxs("div", { className: "flex gap-1", children: [_jsx("input", { type: "number", min: 1, max: rows, value: formStartRow, onChange: (e) => setFormStartRow(parseInt(e.target.value) || 1), placeholder: "R", className: "w-1/2 px-1 py-1 text-xs border border-gray-300 rounded text-center" }), _jsx("input", { type: "number", min: 1, max: cols, value: formStartCol, onChange: (e) => setFormStartCol(parseInt(e.target.value) || 1), placeholder: "C", className: "w-1/2 px-1 py-1 text-xs border border-gray-300 rounded text-center" })] })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[11px] font-semibold text-gray-600 mb-0.5", children: "C\u00E2u g\u1EE3i \u00FD (Clue)" }), _jsx("input", { type: "text", value: formClue, onChange: (e) => setFormClue(e.target.value), placeholder: "vd: Spain, Capital of France...", className: "w-full px-3 py-1.5 text-sm border border-gray-300 rounded outline-none", required: true })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[11px] font-semibold text-gray-600 mb-0.5", children: "T\u1EEB \u0111\u00E1p \u00E1n (Answer Word)" }), _jsx("input", { type: "text", value: formAnswer, onChange: (e) => setFormAnswer(e.target.value), placeholder: "vd: SPANISH", className: "w-full px-3 py-1.5 text-sm border border-gray-300 rounded outline-none uppercase font-semibold", required: true })] }), _jsxs("button", { type: "submit", className: "mt-1 flex items-center justify-center gap-1.5 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm font-semibold transition", children: [editingClueId ? _jsx(Check, { className: "h-4 w-4" }) : _jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { children: editingClueId ? 'Cập nhật gợi ý' : 'Thêm vào danh sách' })] })] }), _jsxs("div", { className: "flex flex-col gap-2 max-h-[420px] overflow-y-auto pr-1", children: [_jsxs("span", { className: "text-xs font-bold text-gray-600", children: ["Danh s\u00E1ch (", clues.length, " t\u1EEB):"] }), clues.length === 0 && (_jsx("p", { className: "text-xs text-gray-400 italic", children: "Ch\u01B0a c\u00F3 g\u1EE3i \u00FD n\u00E0o. H\u00E3y th\u00EAm g\u1EE3i \u00FD \u1EDF tr\u00EAn." })), clues.map((c) => (_jsxs("div", { className: "flex items-center justify-between p-2.5 bg-white rounded-lg border border-gray-200 hover:border-blue-300 transition text-sm shadow-sm", children: [_jsxs("div", { className: "flex items-center gap-2 flex-1 min-w-0", children: [_jsx("span", { className: `flex items-center justify-center h-6 w-6 rounded text-xs font-bold text-white ${c.direction === 'across' ? 'bg-blue-600' : 'bg-purple-600'}`, children: c.number }), _jsxs("div", { className: "flex flex-col min-w-0 flex-1", children: [_jsx("span", { className: "font-semibold text-gray-800 truncate", children: c.clue }), _jsxs("span", { className: "text-xs text-gray-500 flex items-center gap-2", children: [_jsx("span", { className: "font-bold text-blue-700", children: c.answer }), _jsxs("span", { children: ["(", c.length, " ch\u1EEF)"] }), _jsxs("span", { children: ["[", c.direction === 'across' ? '→ Ngang' : '↓ Dọc', " (", c.startRow, ",", c.startCol, ")]"] })] })] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => handleEditClue(c), className: "p-1 text-gray-500 hover:text-blue-600 rounded", title: "S\u1EEDa", children: _jsx(Edit2, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", onClick: () => handleDeleteClue(c.id), className: "p-1 text-gray-500 hover:text-red-600 rounded", title: "X\u00F3a", children: _jsx(Trash2, { className: "h-4 w-4" }) })] })] }, c.id)))] })] }), _jsxs("div", { className: "lg:col-span-7 flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-xs font-bold text-gray-700", children: "Ma tr\u1EADn l\u01B0\u1EDBi \u00F4 ch\u1EEF tr\u1EF1c quan:" }), conflicts.length > 0 && (_jsxs("span", { className: "flex items-center gap-1 text-xs font-bold text-red-600 bg-red-50 px-2 py-0.5 rounded border border-red-200", children: [_jsx(AlertCircle, { className: "h-3.5 w-3.5" }), _jsxs("span", { children: ["C\u00F3 ", conflicts.length, " \u00F4 b\u1ECB xung \u0111\u1ED9t ch\u1EEF c\u00E1i!"] })] }))] }), _jsx("div", { className: "flex justify-center overflow-x-auto p-4 bg-gray-100 rounded-xl border border-gray-300 min-h-[380px]", children: _jsx("div", { className: "inline-grid gap-1", style: {
|
|
190
|
+
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
191
|
+
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
192
|
+
const r = rIdx + 1;
|
|
193
|
+
return Array.from({ length: cols }).map((_, cIdx) => {
|
|
194
|
+
const c = cIdx + 1;
|
|
195
|
+
const key = `${r}-${c}`;
|
|
196
|
+
const cellEntries = cellMap.get(key);
|
|
197
|
+
const clueNums = clueNumberMap.get(key);
|
|
198
|
+
const isConflict = conflicts.includes(key);
|
|
199
|
+
if (!cellEntries || cellEntries.length === 0) {
|
|
200
|
+
return (_jsx("button", { type: "button", onClick: () => {
|
|
201
|
+
setFormStartRow(r);
|
|
202
|
+
setFormStartCol(c);
|
|
203
|
+
}, title: `Ô trống (${r},${c}) - Click để chọn toạ độ bắt đầu`, className: "h-8 w-8 sm:h-9 sm:w-9 md:h-10 md:w-10 bg-transparent hover:bg-gray-200/60 rounded flex items-center justify-center transition text-[10px] text-gray-300 hover:text-gray-500", children: "\u00B7" }, key));
|
|
204
|
+
}
|
|
205
|
+
const displayLetter = cellEntries[0]?.letter || '';
|
|
206
|
+
return (_jsxs("div", { className: `relative flex items-center justify-center rounded border-2 font-bold shadow-sm h-8 w-8 sm:h-9 sm:w-9 md:h-10 md:w-10 text-sm sm:text-base md:text-lg select-none transition ${isConflict
|
|
207
|
+
? 'border-red-500 bg-red-100 text-red-700 animate-pulse'
|
|
208
|
+
: cellEntries.length > 1
|
|
209
|
+
? 'border-indigo-400 bg-indigo-50 text-indigo-900'
|
|
210
|
+
: 'border-gray-400 bg-white text-gray-800'}`, children: [clueNums && clueNums.length > 0 && (_jsx("span", { className: "absolute top-0.5 left-0.5 text-[9px] font-bold text-gray-500 leading-none", children: clueNums.join('/') })), _jsx("span", { children: displayLetter })] }, key));
|
|
211
|
+
});
|
|
212
|
+
}) }) })] })] })] }));
|
|
213
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CrosswordPuzzleCreator } from './CrosswordPuzzleCreator';
|
|
2
|
+
export const crosswordPuzzleRegistration = {
|
|
3
|
+
component: CrosswordPuzzleCreator,
|
|
4
|
+
useRawInitialData: true,
|
|
5
|
+
getExtraProps: (ctx) => ({
|
|
6
|
+
onUnsavedChangesChange: ctx.onUnsavedChangesChange,
|
|
7
|
+
}),
|
|
8
|
+
wrapOnSave: (data, ctx) => ({
|
|
9
|
+
type: ctx.questionType,
|
|
10
|
+
content: ctx.state.content,
|
|
11
|
+
points: data.points ?? ctx.state.points ?? 1,
|
|
12
|
+
level: ctx.state.level,
|
|
13
|
+
difficulty: ctx.state.difficulty,
|
|
14
|
+
skill: ctx.state.skill,
|
|
15
|
+
...(data.content !== undefined ? { content: data.content } : {}),
|
|
16
|
+
answer: data.answer ?? data,
|
|
17
|
+
explanation: data.explanation || '',
|
|
18
|
+
}),
|
|
19
|
+
wrapOnChange: (data, ctx) => ({
|
|
20
|
+
type: ctx.questionType,
|
|
21
|
+
points: data.points ?? ctx.state.points ?? 1,
|
|
22
|
+
level: ctx.state.level,
|
|
23
|
+
difficulty: ctx.state.difficulty,
|
|
24
|
+
skill: ctx.state.skill,
|
|
25
|
+
content: data.content,
|
|
26
|
+
answer: data.answer,
|
|
27
|
+
explanation: data.explanation || '',
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const transformCrosswordPuzzle = (question) => {
|
|
2
|
+
const contentData = question.content;
|
|
3
|
+
const answerData = question.answer;
|
|
4
|
+
let content;
|
|
5
|
+
let correctAnswer;
|
|
6
|
+
if (contentData?.gridSize && contentData?.clues) {
|
|
7
|
+
content = {
|
|
8
|
+
title: contentData.title || '',
|
|
9
|
+
gridSize: {
|
|
10
|
+
rows: Number(contentData.gridSize.rows) || 12,
|
|
11
|
+
cols: Number(contentData.gridSize.cols) || 12,
|
|
12
|
+
},
|
|
13
|
+
clues: {
|
|
14
|
+
across: Array.isArray(contentData.clues.across) ? contentData.clues.across : [],
|
|
15
|
+
down: Array.isArray(contentData.clues.down) ? contentData.clues.down : [],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
correctAnswer = {
|
|
19
|
+
answers: answerData?.answers || {},
|
|
20
|
+
caseSensitive: answerData?.caseSensitive || false,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
else if (Array.isArray(contentData?.clues)) {
|
|
24
|
+
const formClues = contentData.clues;
|
|
25
|
+
const across = formClues
|
|
26
|
+
.filter((c) => c.direction === 'across')
|
|
27
|
+
.map((c) => ({
|
|
28
|
+
number: Number(c.number),
|
|
29
|
+
clue: String(c.clue || '').trim(),
|
|
30
|
+
startRow: Number(c.startRow) || 1,
|
|
31
|
+
startCol: Number(c.startCol) || 1,
|
|
32
|
+
length: Number(c.length) || (c.answer ? String(c.answer).trim().length : 1),
|
|
33
|
+
}));
|
|
34
|
+
const down = formClues
|
|
35
|
+
.filter((c) => c.direction === 'down')
|
|
36
|
+
.map((c) => ({
|
|
37
|
+
number: Number(c.number),
|
|
38
|
+
clue: String(c.clue || '').trim(),
|
|
39
|
+
startRow: Number(c.startRow) || 1,
|
|
40
|
+
startCol: Number(c.startCol) || 1,
|
|
41
|
+
length: Number(c.length) || (c.answer ? String(c.answer).trim().length : 1),
|
|
42
|
+
}));
|
|
43
|
+
content = {
|
|
44
|
+
title: contentData.title || '',
|
|
45
|
+
gridSize: {
|
|
46
|
+
rows: Number(contentData.rows) || 12,
|
|
47
|
+
cols: Number(contentData.cols) || 12,
|
|
48
|
+
},
|
|
49
|
+
clues: { across, down },
|
|
50
|
+
};
|
|
51
|
+
const answers = {};
|
|
52
|
+
for (const c of formClues) {
|
|
53
|
+
const key = `${c.direction}-${c.number}`;
|
|
54
|
+
if (c.answer) {
|
|
55
|
+
answers[key] = String(c.answer).trim().toUpperCase();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
correctAnswer = {
|
|
59
|
+
answers,
|
|
60
|
+
caseSensitive: contentData.caseSensitive || false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
content = {
|
|
65
|
+
gridSize: { rows: 12, cols: 12 },
|
|
66
|
+
clues: { across: [], down: [] },
|
|
67
|
+
};
|
|
68
|
+
correctAnswer = { answers: {}, caseSensitive: false };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
apiContent: content,
|
|
72
|
+
apiCorrectAnswer: correctAnswer,
|
|
73
|
+
explanation: answerData?.explanation || question.explanation || '',
|
|
74
|
+
};
|
|
75
|
+
};
|