@tinyweb_dev/oe-exam-sdk 1.0.8 → 1.0.10
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/take/components/QuestionRenderer.js +1 -5
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversCrossOutWordGroupRenderer.js +24 -10
- package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversFillInBlankGroupRenderer.js +2 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQaGroupRenderer.js +12 -2
- package/dist/components/exams/take/hooks/useSpontaneousQaGroupRendererSetup.js +2 -1
- package/dist/components/exams/take/utils/answer-transformers.js +16 -1
- package/dist/components/exams/take/utils/cross-out-word-group.transformer.js +4 -6
- package/dist/components/exams/take/utils/question-transformers.d.ts +5 -0
- package/dist/components/exams/take/utils/question-transformers.js +6 -0
- package/dist/components/questions/_shared/components/compact/KidChoiceRow.d.ts +12 -0
- package/dist/components/questions/_shared/components/compact/KidChoiceRow.js +33 -0
- package/dist/components/questions/_shared/components/compact/KidInstruction.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidInstruction.js +9 -0
- package/dist/components/questions/_shared/components/compact/KidPrompt.d.ts +5 -0
- package/dist/components/questions/_shared/components/compact/KidPrompt.js +9 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.d.ts +12 -0
- package/dist/components/questions/_shared/components/compact/KidQuestionCard.js +7 -0
- package/dist/components/questions/_shared/components/compact/index.d.ts +4 -0
- package/dist/components/questions/_shared/components/compact/index.js +4 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +3 -0
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +17 -0
- package/dist/components/questions/_shared/types/cross-out-word-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/crossword-puzzle.type.d.ts +5 -1
- package/dist/components/questions/_shared/types/spontaneous-qa-group.type.d.ts +17 -0
- package/dist/components/questions/_shared/types/true-false-correct-group.type.d.ts +1 -0
- package/dist/components/questions/_shared/types/true-false-group.type.d.ts +1 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +18 -19
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.d.ts +1 -1
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +16 -17
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +7 -4
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +2 -0
- package/dist/components/questions/types/answer-the-question-group/transform.js +12 -5
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +17 -11
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +15 -42
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +15 -36
- package/dist/components/questions/types/choose-the-correct-answer-group/ChooseTheCorrectAnswerGroupClient.js +9 -32
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +1 -1
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +76 -11
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +54 -6
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +4 -1
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +39 -2
- package/dist/components/questions/types/choose-then-answer-group/transform.js +27 -14
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.d.ts +1 -1
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupClient.js +76 -7
- package/dist/components/questions/types/cross-out-word-group/CrossOutWordGroupCreator.js +24 -44
- package/dist/components/questions/types/cross-out-word-group/index.d.ts +1 -0
- package/dist/components/questions/types/cross-out-word-group/index.js +1 -0
- package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js +9 -9
- package/dist/components/questions/types/cross-out-word-group/tokenize.d.ts +2 -0
- package/dist/components/questions/types/cross-out-word-group/tokenize.js +11 -0
- package/dist/components/questions/types/cross-out-word-group/transform.js +2 -4
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.d.ts +2 -1
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleClient.js +20 -8
- package/dist/components/questions/types/crossword-puzzle/CrosswordPuzzleCreator.js +68 -11
- package/dist/components/questions/types/crossword-puzzle/transform.js +59 -30
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +26 -31
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.d.ts +6 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankAnswerKey.js +17 -0
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupClient.js +32 -28
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupCreator.js +109 -26
- package/dist/components/questions/types/fill-in-blank-group/FillInBlankGroupMediaField.js +21 -11
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.d.ts +1 -0
- package/dist/components/questions/types/fill-in-blank-group/blank-utils.js +4 -1
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +32 -31
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.d.ts +12 -2
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixClient.js +188 -46
- package/dist/components/questions/types/find-words-in-matrix/FindWordsInMatrixCreator.js +6 -2
- package/dist/components/questions/types/find-words-in-matrix/transform.js +5 -0
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +4 -3
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +11 -30
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +7 -6
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +2 -1
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +14 -13
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +4 -3
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +11 -10
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +4 -3
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +13 -11
- package/dist/components/questions/types/match-columns-to-make-sentences/MatchColumnsToMakeSentencesClient.js +17 -14
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureClient.js +16 -12
- package/dist/components/questions/types/match-word-to-picture/MatchWordToPictureGroupClient.js +14 -14
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +13 -13
- package/dist/components/questions/types/matching-with-lines-group/MatchingWithLinesGroupClient.js +7 -6
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +8 -26
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +2 -1
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +6 -5
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardClient.js +2 -1
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +5 -4
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.d.ts +1 -1
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupClient.js +54 -11
- package/dist/components/questions/types/spontaneous-qa-group/SpontaneousQaGroupCreator.js +11 -7
- package/dist/components/questions/types/spontaneous-qa-group/creator-helpers.js +8 -2
- package/dist/components/questions/types/spontaneous-qa-group/flatten.d.ts +1 -0
- package/dist/components/questions/types/spontaneous-qa-group/flatten.js +4 -2
- package/dist/components/questions/types/spontaneous-qa-group/map-spontaneous-qa-group-data.js +5 -3
- package/dist/components/questions/types/spontaneous-qa-group/register.js +5 -2
- package/dist/components/questions/types/spontaneous-qa-group/transform.js +6 -2
- package/dist/components/questions/types/true-false/TrueFalseClient.js +5 -54
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.d.ts +1 -1
- package/dist/components/questions/types/true-false-correct-group/TrueFalseCorrectGroupClient.js +39 -19
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.d.ts +1 -1
- package/dist/components/questions/types/true-false-group/TrueFalseGroupClient.js +20 -19
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +76 -81
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +17 -16
- package/dist/components/questions/types/word-order-and-match-group/WordOrderAndMatchGroupClient.js +31 -36
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +15 -14
- package/dist/components/questions/types/word-ordering-group/WordOrderingGroupClient.js +9 -8
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +16 -13
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/AnswerTheQuestionGroupViewer.js +12 -5
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +4 -3
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.d.ts +3 -1
- package/dist/components/questions/viewer/CrossOutWordGroupViewer.js +12 -2
- package/dist/components/questions/viewer/FillInBlankGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/FillInBlankGroupViewer.js +13 -7
- package/dist/components/questions/viewer/QuestionViewer.d.ts +9 -1
- package/dist/components/questions/viewer/QuestionViewer.js +23 -10
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.d.ts +7 -1
- package/dist/components/questions/viewer/SpontaneousQaGroupViewer.js +14 -2
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.d.ts +3 -2
- package/dist/components/questions/viewer/TrueFalseCorrectGroupViewer.js +14 -4
- package/dist/components/questions/viewer/TrueFalseGroupViewer.d.ts +5 -2
- package/dist/components/questions/viewer/TrueFalseGroupViewer.js +14 -4
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.d.ts +6 -2
- package/dist/components/questions/viewer/WordOrderAndMatchGroupViewer.js +26 -6
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +1 -1
- package/dist/components/results/renderers/review-question-body-movers.js +1 -1
- package/dist/components/results/review-data.js +8 -1
- package/dist/shared/lib/utils/question-reverse-transform.js +39 -5
- package/dist/shared/types/questions/answer-the-question-group.d.ts +8 -3
- package/dist/shared/types/questions/answer-the-question-group.js +5 -0
- package/dist/shared/types/questions/choose-then-answer-group.d.ts +3 -0
- package/dist/shared/types/questions/crossword-puzzle.d.ts +1 -0
- package/dist/shared/types/questions/find-words-in-matrix.d.ts +1 -0
- package/package.json +1 -1
|
@@ -13,6 +13,8 @@ import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
|
13
13
|
import { HelpCircle, Plus, Star, Trash2 } from 'lucide-react';
|
|
14
14
|
import { CompactCreatorHeader, CompactExplanationToggle, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
|
|
15
15
|
import { CrossOutWordGroupClient } from './CrossOutWordGroupClient';
|
|
16
|
+
import { createCrossOutAnswer, createEmptyCrossOutItem, DEFAULT_CROSS_OUT_INSTRUCTION, } from './map-cross-out-word-group-data';
|
|
17
|
+
import { tokenizeCrossOutQuestion } from './tokenize';
|
|
16
18
|
function parsePointsValue(raw, allowZero = false) {
|
|
17
19
|
if (raw === '')
|
|
18
20
|
return allowZero ? 0 : 1;
|
|
@@ -22,43 +24,9 @@ function parsePointsValue(raw, allowZero = false) {
|
|
|
22
24
|
const min = allowZero ? 0 : 1;
|
|
23
25
|
return Math.max(min, parsed);
|
|
24
26
|
}
|
|
25
|
-
const DEFAULT_INSTRUCTION = 'Cross out the extra or incorrect word. If the word is incorrect, write the correct word. If the line is correct, leave it blank.';
|
|
26
|
-
function tokenize(sentence) {
|
|
27
|
-
return sentence.trim().split(/\s+/).filter(Boolean);
|
|
28
|
-
}
|
|
29
|
-
function createAnswer(type, crossOut = '', replaceWith = '', wordIndex) {
|
|
30
|
-
const parsedIndex = typeof wordIndex === 'number' && Number.isInteger(wordIndex) && wordIndex >= 0
|
|
31
|
-
? wordIndex
|
|
32
|
-
: wordIndex === null
|
|
33
|
-
? null
|
|
34
|
-
: undefined;
|
|
35
|
-
if (type === 'CROSS_OUT_AND_REPLACE') {
|
|
36
|
-
return {
|
|
37
|
-
type,
|
|
38
|
-
...(parsedIndex !== undefined ? { wordIndex: parsedIndex } : {}),
|
|
39
|
-
crossOut,
|
|
40
|
-
replaceWith,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
type: 'CROSS_OUT',
|
|
45
|
-
...(parsedIndex !== undefined ? { wordIndex: parsedIndex } : {}),
|
|
46
|
-
crossOut,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function createEmptyItem(questionNumber) {
|
|
50
|
-
return {
|
|
51
|
-
type: 'CROSS_OUT',
|
|
52
|
-
question: '',
|
|
53
|
-
correctAnswer: createAnswer('CROSS_OUT'),
|
|
54
|
-
isExample: false,
|
|
55
|
-
points: 1,
|
|
56
|
-
questionNumber,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
27
|
function normalizeItems(rawItems) {
|
|
60
28
|
if (!Array.isArray(rawItems) || rawItems.length === 0) {
|
|
61
|
-
return [
|
|
29
|
+
return [createEmptyCrossOutItem(1)];
|
|
62
30
|
}
|
|
63
31
|
return rawItems.map((item, index) => {
|
|
64
32
|
const type = item.type === 'CROSS_OUT_AND_REPLACE' ||
|
|
@@ -66,7 +34,7 @@ function normalizeItems(rawItems) {
|
|
|
66
34
|
? 'CROSS_OUT_AND_REPLACE'
|
|
67
35
|
: 'CROSS_OUT';
|
|
68
36
|
const rawIndex = item.correctAnswer?.wordIndex;
|
|
69
|
-
const correctAnswer =
|
|
37
|
+
const correctAnswer = createCrossOutAnswer(type, item.correctAnswer?.crossOut || '', item.correctAnswer?.replaceWith || '', rawIndex);
|
|
70
38
|
return {
|
|
71
39
|
type,
|
|
72
40
|
question: item.question || '',
|
|
@@ -81,7 +49,7 @@ function sumPoints(items) {
|
|
|
81
49
|
return items.reduce((total, item) => item.isExample ? total : total + (typeof item.points === 'number' ? item.points : 1), 0);
|
|
82
50
|
}
|
|
83
51
|
export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
|
|
84
|
-
const [instruction, setInstruction] = useState(initialData?.instruction ||
|
|
52
|
+
const [instruction, setInstruction] = useState(initialData?.instruction || DEFAULT_CROSS_OUT_INSTRUCTION);
|
|
85
53
|
const [items, setItems] = useState(() => normalizeItems(initialData?.items));
|
|
86
54
|
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
87
55
|
const [isClientMode, setIsClientMode] = useState(false);
|
|
@@ -121,7 +89,20 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
121
89
|
if (!item.question.trim()) {
|
|
122
90
|
nextErrors.push(`Câu ${index + 1}: Nội dung câu không được để trống.`);
|
|
123
91
|
}
|
|
92
|
+
const wordIndex = item.correctAnswer.wordIndex;
|
|
93
|
+
if (wordIndex === undefined) {
|
|
94
|
+
nextErrors.push(`Câu ${index + 1}: Cần chọn từ cần gạch hoặc để trống nếu câu đúng (wordIndex).`);
|
|
95
|
+
}
|
|
96
|
+
if (typeof wordIndex === 'number') {
|
|
97
|
+
const tokens = tokenizeCrossOutQuestion(item.question);
|
|
98
|
+
if (tokens[wordIndex] !== item.correctAnswer.crossOut) {
|
|
99
|
+
nextErrors.push(`Câu ${index + 1}: Từ gạch phải khớp token tại wordIndex.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
124
102
|
if (item.type === 'CROSS_OUT_AND_REPLACE') {
|
|
103
|
+
if (typeof wordIndex !== 'number') {
|
|
104
|
+
nextErrors.push(`Câu ${index + 1}: Cần chọn từ cần gạch (wordIndex).`);
|
|
105
|
+
}
|
|
125
106
|
if (!item.correctAnswer.crossOut.trim()) {
|
|
126
107
|
nextErrors.push(`Câu ${index + 1}: Cần chọn hoặc nhập từ cần gạch (crossOut).`);
|
|
127
108
|
}
|
|
@@ -160,7 +141,7 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
160
141
|
next[index] = {
|
|
161
142
|
...current,
|
|
162
143
|
type,
|
|
163
|
-
correctAnswer:
|
|
144
|
+
correctAnswer: createCrossOutAnswer(type, current.correctAnswer.crossOut, current.correctAnswer.replaceWith || '', current.correctAnswer.wordIndex),
|
|
164
145
|
};
|
|
165
146
|
return next;
|
|
166
147
|
});
|
|
@@ -174,7 +155,7 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
174
155
|
const type = current.type;
|
|
175
156
|
next[index] = {
|
|
176
157
|
...current,
|
|
177
|
-
correctAnswer:
|
|
158
|
+
correctAnswer: createCrossOutAnswer(type, patch.crossOut !== undefined ? patch.crossOut : current.correctAnswer.crossOut, patch.replaceWith !== undefined
|
|
178
159
|
? patch.replaceWith
|
|
179
160
|
: current.correctAnswer.replaceWith || '', patch.wordIndex !== undefined ? patch.wordIndex : current.correctAnswer.wordIndex),
|
|
180
161
|
};
|
|
@@ -194,15 +175,14 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
194
175
|
});
|
|
195
176
|
}
|
|
196
177
|
else {
|
|
197
|
-
const cleanWord = token.replace(/[^\p{L}\p{N}'-]/gu, '') || token;
|
|
198
178
|
updateItemAnswer(itemIndex, {
|
|
199
179
|
wordIndex: tokenIndex,
|
|
200
|
-
crossOut:
|
|
180
|
+
crossOut: token,
|
|
201
181
|
});
|
|
202
182
|
}
|
|
203
183
|
};
|
|
204
184
|
const addItem = () => {
|
|
205
|
-
setItems((prev) => [...prev,
|
|
185
|
+
setItems((prev) => [...prev, createEmptyCrossOutItem(prev.length + 1)]);
|
|
206
186
|
};
|
|
207
187
|
const removeItem = (index) => {
|
|
208
188
|
setItems((prev) => {
|
|
@@ -217,8 +197,8 @@ export function CrossOutWordGroupCreator({ initialData, onChange, externalErrors
|
|
|
217
197
|
if (isClientMode) {
|
|
218
198
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(CrossOutWordGroupClient, { questionData: currentPayload })] }));
|
|
219
199
|
}
|
|
220
|
-
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsx(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: "G\u1EA1ch t\u1EEB / S\u1EEDa t\u1EEB (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder:
|
|
221
|
-
const tokens =
|
|
200
|
+
return (_jsxs("div", { className: "space-y-3", children: [displayErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-red-200 bg-red-50 p-2 text-xs text-red-700", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: displayErrors.map((err, i) => (_jsx("li", { children: err }, i))) }) })), _jsx(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(HelpCircle, { className: "h-3.5 w-3.5" }), title: "G\u1EA1ch t\u1EEB / S\u1EEDa t\u1EEB (Nh\u00F3m)", preview: { onClick: () => setIsClientMode(true) } }), children: _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "instruction", className: "text-sm font-semibold text-gray-800", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx(Input, { id: "instruction", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: DEFAULT_CROSS_OUT_INSTRUCTION, className: "h-10 border-gray-200 bg-white" })] }) }), _jsxs(Card, { className: "overflow-hidden border-0 bg-white shadow-sm", children: [_jsx(CardHeader, { className: "px-3 py-2", children: _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs(CardTitle, { className: "text-sm font-semibold text-gray-800", children: ["Danh s\u00E1ch c\u00E2u (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addItem, className: "h-8 gap-1.5 px-2.5 text-xs", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), "Th\u00EAm c\u00E2u"] })] }) }), _jsxs(CardContent, { className: "space-y-2.5 px-3 pb-3", children: [items.map((item, index) => {
|
|
201
|
+
const tokens = tokenizeCrossOutQuestion(item.question);
|
|
222
202
|
const selectedWordIndex = item.correctAnswer.wordIndex;
|
|
223
203
|
return (_jsxs("div", { className: "space-y-2.5 rounded-md border border-gray-200 bg-white px-3 py-2.5", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex h-7 w-7 items-center justify-center rounded-full bg-blue-100 text-xs font-semibold text-blue-700", children: item.questionNumber || index + 1 }), _jsxs("span", { className: "text-sm font-medium text-gray-700", children: ["C\u00E2u ", index + 1] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("div", { className: "w-52", children: _jsxs(Select, { value: item.type, onValueChange: (value) => updateItemType(index, value), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Lo\u1EA1i c\u00E2u" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "CROSS_OUT", children: "Ch\u1EC9 g\u1EA1ch (CROSS_OUT)" }), _jsx(SelectItem, { value: "CROSS_OUT_AND_REPLACE", children: "G\u1EA1ch + thay (CROSS_OUT_AND_REPLACE)" })] })] }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `example-${index}`, checked: item.isExample || false, onCheckedChange: (checked) => updateItem(index, {
|
|
224
204
|
isExample: checked,
|
package/dist/components/questions/types/cross-out-word-group/map-cross-out-word-group-data.js
CHANGED
|
@@ -14,20 +14,18 @@ function normalizeMode(type) {
|
|
|
14
14
|
export function createCrossOutAnswer(type, crossOut = '', replaceWith = '', wordIndex) {
|
|
15
15
|
const parsedIndex = typeof wordIndex === 'number' && Number.isInteger(wordIndex) && wordIndex >= 0
|
|
16
16
|
? wordIndex
|
|
17
|
-
:
|
|
18
|
-
? null
|
|
19
|
-
: undefined;
|
|
17
|
+
: null;
|
|
20
18
|
if (type === 'CROSS_OUT_AND_REPLACE') {
|
|
21
19
|
return {
|
|
22
20
|
type,
|
|
23
|
-
|
|
21
|
+
wordIndex: parsedIndex,
|
|
24
22
|
crossOut,
|
|
25
23
|
replaceWith,
|
|
26
24
|
};
|
|
27
25
|
}
|
|
28
26
|
return {
|
|
29
27
|
type: 'CROSS_OUT',
|
|
30
|
-
|
|
28
|
+
wordIndex: parsedIndex,
|
|
31
29
|
crossOut,
|
|
32
30
|
};
|
|
33
31
|
}
|
|
@@ -47,10 +45,12 @@ export function mapCrossOutWordGroupItem(item, index, fallbackAnswer) {
|
|
|
47
45
|
const correctAnswerRaw = asRecord(record.correctAnswer);
|
|
48
46
|
const answerObj = isRecord(correctAnswerRaw.answer)
|
|
49
47
|
? correctAnswerRaw.answer
|
|
50
|
-
:
|
|
51
|
-
?
|
|
52
|
-
:
|
|
53
|
-
|
|
48
|
+
: 'crossOut' in correctAnswerRaw || 'wordIndex' in correctAnswerRaw
|
|
49
|
+
? correctAnswerRaw
|
|
50
|
+
: isRecord(fallbackAnswer)
|
|
51
|
+
? fallbackAnswer
|
|
52
|
+
: {};
|
|
53
|
+
const type = normalizeMode(content.type || record.type || answerObj.type);
|
|
54
54
|
const isExample = record.isExample === true;
|
|
55
55
|
const rawIndex = typeof answerObj.wordIndex === 'number'
|
|
56
56
|
? answerObj.wordIndex
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function normalizeCrossOutQuestionText(question) {
|
|
2
|
+
return question
|
|
3
|
+
.replace(/\\n/g, ' ')
|
|
4
|
+
.replace(/\r\n|\r|\n/g, ' ')
|
|
5
|
+
.replace(/\s+/g, ' ')
|
|
6
|
+
.trim();
|
|
7
|
+
}
|
|
8
|
+
export function tokenizeCrossOutQuestion(question) {
|
|
9
|
+
const normalized = normalizeCrossOutQuestionText(question);
|
|
10
|
+
return normalized ? normalized.split(' ').filter(Boolean) : [];
|
|
11
|
+
}
|
|
@@ -16,12 +16,10 @@ function normalizeAnswer(answer, fallbackType) {
|
|
|
16
16
|
const rawIndex = answer?.wordIndex;
|
|
17
17
|
const wordIndex = typeof rawIndex === 'number' && Number.isInteger(rawIndex) && rawIndex >= 0
|
|
18
18
|
? rawIndex
|
|
19
|
-
:
|
|
20
|
-
? null
|
|
21
|
-
: undefined;
|
|
19
|
+
: null;
|
|
22
20
|
const result = {
|
|
23
21
|
type,
|
|
24
|
-
|
|
22
|
+
wordIndex,
|
|
25
23
|
crossOut: typeof answer?.crossOut === 'string' ? answer.crossOut.trim() : '',
|
|
26
24
|
};
|
|
27
25
|
if (type === 'CROSS_OUT_AND_REPLACE') {
|
|
@@ -5,7 +5,8 @@ interface CrosswordPuzzleClientProps {
|
|
|
5
5
|
correctAnswer?: CrosswordPuzzleCorrectAnswer;
|
|
6
6
|
isReviewMode?: boolean;
|
|
7
7
|
userAnswers?: Record<string, string>;
|
|
8
|
+
questionNumber?: number;
|
|
8
9
|
onAnswerChange?: (key: string, value: string) => void;
|
|
9
10
|
}
|
|
10
|
-
export declare function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode, userAnswers, }: CrosswordPuzzleClientProps): React.JSX.Element;
|
|
11
|
+
export declare function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode, userAnswers, questionNumber, onAnswerChange, }: CrosswordPuzzleClientProps): React.JSX.Element;
|
|
11
12
|
export {};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
-
export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = false, userAnswers = {}, }) {
|
|
4
|
+
export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = false, userAnswers = {}, questionNumber, onAnswerChange, }) {
|
|
5
5
|
const rows = content?.gridSize?.rows || 12;
|
|
6
6
|
const cols = content?.gridSize?.cols || 12;
|
|
7
|
+
const instructionText = (content?.instruction || content?.title || '').trim();
|
|
8
|
+
const displayNumber = questionNumber ?? 1;
|
|
7
9
|
const acrossClues = useMemo(() => content?.clues?.across || [], [content?.clues?.across]);
|
|
8
10
|
const downClues = useMemo(() => content?.clues?.down || [], [content?.clues?.down]);
|
|
9
11
|
// Build grid cell mapping
|
|
@@ -30,14 +32,14 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
|
|
|
30
32
|
}
|
|
31
33
|
return { cellMap: cMap, clueNumberMap: numMap };
|
|
32
34
|
}, [acrossClues, downClues]);
|
|
33
|
-
return (_jsxs("div", { className: "flex flex-col
|
|
35
|
+
return (_jsxs("div", { className: "flex flex-col items-stretch gap-3 text-left", children: [_jsxs("p", { className: "w-full text-left text-sm font-semibold text-gray-800", children: [_jsxs("span", { className: "font-bold", children: [displayNumber, "."] }), instructionText ? ` ${instructionText}` : ''] }), _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
36
|
const answer = correctAnswer?.answers?.[`across-${c.number}`];
|
|
35
37
|
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: [
|
|
38
|
+
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: [_jsxs("span", { className: "text-gray-800", children: [c.clue, c.isExample && (_jsx("span", { className: "ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-bold text-amber-700", children: "Example" }))] }), !isReviewMode && onAnswerChange && !c.isExample ? (_jsx("input", { type: "text", value: userAns || '', maxLength: c.length, onChange: (event) => onAnswerChange(`across-${c.number}`, event.target.value.toUpperCase()), placeholder: `${c.length} chữ`, className: "mt-1.5 w-full rounded border border-blue-200 px-2 py-1 text-sm font-semibold uppercase tracking-wider outline-none focus:border-blue-500" })) : null, 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
39
|
}) })] }), _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
40
|
const answer = correctAnswer?.answers?.[`down-${c.number}`];
|
|
39
41
|
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: [
|
|
42
|
+
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: [_jsxs("span", { className: "text-gray-800", children: [c.clue, c.isExample && (_jsx("span", { className: "ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-bold text-amber-700", children: "Example" }))] }), !isReviewMode && onAnswerChange && !c.isExample ? (_jsx("input", { type: "text", value: userAns || '', maxLength: c.length, onChange: (event) => onAnswerChange(`down-${c.number}`, event.target.value.toUpperCase()), placeholder: `${c.length} chữ`, className: "mt-1.5 w-full rounded border border-purple-200 px-2 py-1 text-sm font-semibold uppercase tracking-wider outline-none focus:border-purple-500" })) : null, 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
43
|
}) })] })] }), _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
44
|
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
43
45
|
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
@@ -66,8 +68,18 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
|
|
|
66
68
|
previewLetter = word[offset] || '';
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
|
-
else
|
|
70
|
-
|
|
71
|
+
else {
|
|
72
|
+
if (cellInfo.across) {
|
|
73
|
+
const word = userAnswers[`across-${cellInfo.across.number}`] || '';
|
|
74
|
+
previewLetter = word[c - cellInfo.across.startCol] || '';
|
|
75
|
+
}
|
|
76
|
+
if (!previewLetter && cellInfo.down) {
|
|
77
|
+
const word = userAnswers[`down-${cellInfo.down.number}`] || '';
|
|
78
|
+
previewLetter = word[r - cellInfo.down.startRow] || '';
|
|
79
|
+
}
|
|
80
|
+
if (!previewLetter) {
|
|
81
|
+
previewLetter = userAnswers[key] || '';
|
|
82
|
+
}
|
|
71
83
|
}
|
|
72
84
|
const cellBg = isMarked ? 'bg-gray-400 border-gray-400 shadow-sm ring-1 ring-gray-400/50' : 'bg-gray-100 border-gray-300';
|
|
73
85
|
let textColor = isExample
|
|
@@ -80,5 +92,5 @@ export function CrosswordPuzzleClient({ content, correctAnswer, isReviewMode = f
|
|
|
80
92
|
}
|
|
81
93
|
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 ${cellBg} ${textColor}`, 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));
|
|
82
94
|
});
|
|
83
|
-
}) }) })] })] }));
|
|
95
|
+
}) }) })] }), (content?.type === 'HIDDEN_WORD' || content?.markedColumn) && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx("label", { className: "block text-sm font-semibold text-amber-900 mb-1", children: "What's the hidden word?" }), !isReviewMode && onAnswerChange ? (_jsx("input", { type: "text", value: userAnswers['hidden-word'] || '', onChange: (event) => onAnswerChange('hidden-word', event.target.value.toUpperCase()), placeholder: "Hidden word", className: "w-full rounded border border-amber-300 px-3 py-1.5 text-sm font-semibold uppercase tracking-wider outline-none focus:border-amber-500" })) : (_jsxs("div", { className: "text-sm", children: [userAnswers['hidden-word'] && (_jsx("span", { className: "font-semibold text-gray-800", children: userAnswers['hidden-word'] })), isReviewMode && correctAnswer?.answers?.['hidden-word'] && (_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: correctAnswer.answers['hidden-word'] })] }))] }))] }))] }));
|
|
84
96
|
}
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
4
4
|
import { Plus, Trash2, Edit2, Check, AlertCircle, Grid3X3 } from 'lucide-react';
|
|
5
5
|
import { CompactCreatorHeader, CompactFieldCard, CompactPreviewBanner, } from '../../_shared/components/compact';
|
|
6
6
|
import { CrosswordPuzzleClient } from './CrosswordPuzzleClient';
|
|
7
7
|
export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChangesChange, }) {
|
|
8
|
+
const [instruction, setInstruction] = useState(() => {
|
|
9
|
+
return initialData?.content?.instruction || initialData?.instruction || initialData?.answer?.instruction || '';
|
|
10
|
+
});
|
|
11
|
+
const [puzzleType, setPuzzleType] = useState(() => {
|
|
12
|
+
return initialData?.content?.type === 'HIDDEN_WORD' || initialData?.answer?.type === 'HIDDEN_WORD'
|
|
13
|
+
? 'HIDDEN_WORD'
|
|
14
|
+
: 'STANDARD';
|
|
15
|
+
});
|
|
16
|
+
const [markedColumn, setMarkedColumn] = useState(() => {
|
|
17
|
+
return Number(initialData?.content?.markedColumn || initialData?.answer?.markedColumn || 1);
|
|
18
|
+
});
|
|
19
|
+
const [hiddenWord, setHiddenWord] = useState(() => {
|
|
20
|
+
const answers = initialData?.answer?.answers || initialData?.correctAnswer?.answers || {};
|
|
21
|
+
return answers['hidden-word'] || initialData?.answer?.hiddenWord || '';
|
|
22
|
+
});
|
|
8
23
|
const [title, setTitle] = useState(() => {
|
|
9
24
|
return initialData?.content?.title || 'Nationalities Crossword';
|
|
10
25
|
});
|
|
@@ -39,6 +54,7 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
39
54
|
length: a.length,
|
|
40
55
|
direction: 'across',
|
|
41
56
|
answer: answers[`across-${a.number}`] || '',
|
|
57
|
+
isExample: a.isExample === true,
|
|
42
58
|
});
|
|
43
59
|
}
|
|
44
60
|
for (const d of down) {
|
|
@@ -51,6 +67,7 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
51
67
|
length: d.length,
|
|
52
68
|
direction: 'down',
|
|
53
69
|
answer: answers[`down-${d.number}`] || '',
|
|
70
|
+
isExample: d.isExample === true,
|
|
54
71
|
});
|
|
55
72
|
}
|
|
56
73
|
return items;
|
|
@@ -62,6 +79,7 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
62
79
|
const [formAnswer, setFormAnswer] = useState('');
|
|
63
80
|
const [formStartRow, setFormStartRow] = useState(1);
|
|
64
81
|
const [formStartCol, setFormStartCol] = useState(1);
|
|
82
|
+
const [formIsExample, setFormIsExample] = useState(false);
|
|
65
83
|
const { cellMap, clueNumberMap, conflicts } = useMemo(() => {
|
|
66
84
|
const cMap = new Map();
|
|
67
85
|
const numMap = new Map();
|
|
@@ -89,9 +107,17 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
89
107
|
}
|
|
90
108
|
return { cellMap: cMap, clueNumberMap: numMap, conflicts: conflictList };
|
|
91
109
|
}, [clues]);
|
|
110
|
+
const onChangeRef = useRef(onChange);
|
|
111
|
+
const onUnsavedChangesChangeRef = useRef(onUnsavedChangesChange);
|
|
112
|
+
const previousPayloadRef = useRef('');
|
|
113
|
+
const skipUnsavedRef = useRef(true);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
onChangeRef.current = onChange;
|
|
116
|
+
}, [onChange]);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
onUnsavedChangesChangeRef.current = onUnsavedChangesChange;
|
|
119
|
+
}, [onUnsavedChangesChange]);
|
|
92
120
|
useEffect(() => {
|
|
93
|
-
if (!onChange)
|
|
94
|
-
return;
|
|
95
121
|
const across = clues
|
|
96
122
|
.filter((c) => c.direction === 'across')
|
|
97
123
|
.map((c) => ({
|
|
@@ -100,6 +126,7 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
100
126
|
startRow: c.startRow,
|
|
101
127
|
startCol: c.startCol,
|
|
102
128
|
length: c.length || c.answer.trim().length,
|
|
129
|
+
...(c.isExample ? { isExample: true } : {}),
|
|
103
130
|
}));
|
|
104
131
|
const down = clues
|
|
105
132
|
.filter((c) => c.direction === 'down')
|
|
@@ -109,6 +136,7 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
109
136
|
startRow: c.startRow,
|
|
110
137
|
startCol: c.startCol,
|
|
111
138
|
length: c.length || c.answer.trim().length,
|
|
139
|
+
...(c.isExample ? { isExample: true } : {}),
|
|
112
140
|
}));
|
|
113
141
|
const answers = {};
|
|
114
142
|
for (const c of clues) {
|
|
@@ -116,20 +144,37 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
116
144
|
answers[`${c.direction}-${c.number}`] = c.answer.trim().toUpperCase();
|
|
117
145
|
}
|
|
118
146
|
}
|
|
119
|
-
|
|
147
|
+
if (puzzleType === 'HIDDEN_WORD' && hiddenWord.trim()) {
|
|
148
|
+
answers['hidden-word'] = hiddenWord.trim().toUpperCase();
|
|
149
|
+
}
|
|
150
|
+
const payload = {
|
|
120
151
|
content: {
|
|
152
|
+
type: puzzleType,
|
|
121
153
|
title,
|
|
154
|
+
instruction,
|
|
122
155
|
gridSize: { rows, cols },
|
|
156
|
+
...(puzzleType === 'HIDDEN_WORD' ? { markedColumn } : {}),
|
|
123
157
|
clues: { across, down },
|
|
124
158
|
},
|
|
125
159
|
answer: {
|
|
126
160
|
answers,
|
|
127
161
|
caseSensitive,
|
|
162
|
+
hiddenWord: puzzleType === 'HIDDEN_WORD' ? hiddenWord.trim().toUpperCase() : '',
|
|
128
163
|
},
|
|
129
164
|
points,
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
165
|
+
};
|
|
166
|
+
const serialized = JSON.stringify(payload);
|
|
167
|
+
if (serialized === previousPayloadRef.current) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
previousPayloadRef.current = serialized;
|
|
171
|
+
onChangeRef.current?.(payload);
|
|
172
|
+
if (skipUnsavedRef.current) {
|
|
173
|
+
skipUnsavedRef.current = false;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
onUnsavedChangesChangeRef.current?.(true);
|
|
177
|
+
}, [instruction, puzzleType, markedColumn, hiddenWord, title, rows, cols, clues, caseSensitive, points]);
|
|
133
178
|
const handleSaveClue = (e) => {
|
|
134
179
|
e.preventDefault();
|
|
135
180
|
if (!formClue.trim() || !formAnswer.trim())
|
|
@@ -147,6 +192,7 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
147
192
|
startRow: formStartRow,
|
|
148
193
|
startCol: formStartCol,
|
|
149
194
|
length,
|
|
195
|
+
isExample: formIsExample,
|
|
150
196
|
}
|
|
151
197
|
: c));
|
|
152
198
|
setEditingClueId(null);
|
|
@@ -161,11 +207,13 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
161
207
|
startRow: formStartRow,
|
|
162
208
|
startCol: formStartCol,
|
|
163
209
|
length,
|
|
210
|
+
isExample: formIsExample,
|
|
164
211
|
};
|
|
165
212
|
setClues((prev) => [...prev, newClue]);
|
|
166
213
|
}
|
|
167
214
|
setFormClue('');
|
|
168
215
|
setFormAnswer('');
|
|
216
|
+
setFormIsExample(false);
|
|
169
217
|
setFormNumber((prev) => prev + 1);
|
|
170
218
|
};
|
|
171
219
|
const handleEditClue = (c) => {
|
|
@@ -176,6 +224,7 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
176
224
|
setFormAnswer(c.answer);
|
|
177
225
|
setFormStartRow(c.startRow);
|
|
178
226
|
setFormStartCol(c.startCol);
|
|
227
|
+
setFormIsExample(c.isExample === true);
|
|
179
228
|
};
|
|
180
229
|
const handleDeleteClue = (id) => {
|
|
181
230
|
setClues((prev) => prev.filter((c) => c.id !== id));
|
|
@@ -194,6 +243,7 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
194
243
|
startRow: c.startRow,
|
|
195
244
|
startCol: c.startCol,
|
|
196
245
|
length: c.length || c.answer.trim().length,
|
|
246
|
+
...(c.isExample ? { isExample: true } : {}),
|
|
197
247
|
}));
|
|
198
248
|
const down = clues
|
|
199
249
|
.filter((c) => c.direction === 'down')
|
|
@@ -203,6 +253,7 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
203
253
|
startRow: c.startRow,
|
|
204
254
|
startCol: c.startCol,
|
|
205
255
|
length: c.length || c.answer.trim().length,
|
|
256
|
+
...(c.isExample ? { isExample: true } : {}),
|
|
206
257
|
}));
|
|
207
258
|
const answers = {};
|
|
208
259
|
for (const c of clues) {
|
|
@@ -210,20 +261,26 @@ export function CrosswordPuzzleCreator({ initialData, onChange, onUnsavedChanges
|
|
|
210
261
|
answers[`${c.direction}-${c.number}`] = c.answer.trim().toUpperCase();
|
|
211
262
|
}
|
|
212
263
|
}
|
|
264
|
+
if (puzzleType === 'HIDDEN_WORD' && hiddenWord.trim()) {
|
|
265
|
+
answers['hidden-word'] = hiddenWord.trim().toUpperCase();
|
|
266
|
+
}
|
|
213
267
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx(CompactPreviewBanner, { onBack: () => setIsClientMode(false) }), _jsx(CrosswordPuzzleClient, { content: {
|
|
268
|
+
type: puzzleType,
|
|
214
269
|
title,
|
|
270
|
+
instruction,
|
|
215
271
|
gridSize: { rows, cols },
|
|
272
|
+
...(puzzleType === 'HIDDEN_WORD' ? { markedColumn } : {}),
|
|
216
273
|
clues: { across, down },
|
|
217
|
-
}, correctAnswer: { answers, caseSensitive }, isReviewMode: true,
|
|
274
|
+
}, correctAnswer: { answers, caseSensitive }, isReviewMode: true, questionNumber: initialData?.questionNumber ?? 1 })] }));
|
|
218
275
|
}
|
|
219
276
|
return (_jsx("div", { className: "space-y-3", children: _jsx(CompactFieldCard, { header: _jsx(CompactCreatorHeader, { icon: _jsx(Grid3X3, { className: "h-3.5 w-3.5" }), title: "\u00D4 ch\u1EEF (Crossword)", points: {
|
|
220
277
|
value: points,
|
|
221
278
|
onValueChange: setPoints,
|
|
222
|
-
}, preview: { onClick: () => setIsClientMode(true) } }), children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 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" })] })] }), _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: () => {
|
|
279
|
+
}, preview: { onClick: () => setIsClientMode(true) } }), children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 bg-gray-50 rounded-lg border border-gray-200", children: [_jsxs("div", { className: "sm:col-span-2 md:col-span-3", children: [_jsx("label", { className: "block text-xs font-bold text-gray-700 mb-1", children: "H\u01B0\u1EDBng d\u1EABn b\u00E0i l\u00E0m" }), _jsx("input", { type: "text", value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: "vd: Read the information and do the crossword. What's the hidden word?", 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: "D\u1EA1ng \u00F4 ch\u1EEF" }), _jsxs("select", { value: puzzleType, onChange: (e) => setPuzzleType(e.target.value), className: "w-full px-3 py-1.5 text-sm border border-gray-300 rounded outline-none bg-white", children: [_jsx("option", { value: "STANDARD", children: "STANDARD" }), _jsx("option", { value: "HIDDEN_WORD", children: "HIDDEN_WORD" })] })] }), puzzleType === 'HIDDEN_WORD' && (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-gray-700 mb-1", children: "C\u1ED9t t\u1EEB kh\u00F3a (markedColumn)" }), _jsx("input", { type: "number", min: 1, max: cols, value: markedColumn, onChange: (e) => setMarkedColumn(Math.max(1, parseInt(e.target.value) || 1)), className: "w-full px-3 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: "T\u1EEB kh\u00F3a b\u00ED \u1EA9n (hidden-word)" }), _jsx("input", { type: "text", value: hiddenWord, onChange: (e) => setHiddenWord(e.target.value.toUpperCase()), placeholder: "vd: BRITAIN", className: "w-full px-3 py-1.5 text-sm border border-gray-300 rounded outline-none uppercase font-semibold" })] })] })), _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" })] })] }), _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: () => {
|
|
223
280
|
setEditingClueId(null);
|
|
224
281
|
setFormClue('');
|
|
225
282
|
setFormAnswer('');
|
|
226
|
-
}, 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: [
|
|
283
|
+
}, 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("label", { className: "flex items-center gap-2 text-xs text-gray-700 cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: formIsExample, onChange: (e) => setFormIsExample(e.target.checked), className: "rounded text-amber-600 focus:ring-amber-500 h-4 w-4" }), _jsx("span", { children: "Example (kh\u00F4ng t\u00EDnh \u0111i\u1EC3m)" })] }), _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: [_jsxs("span", { className: "font-semibold text-gray-800 truncate", children: [c.clue, c.isExample && (_jsx("span", { className: "ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-bold text-amber-700", children: "Example" }))] }), _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: {
|
|
227
284
|
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
228
285
|
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
229
286
|
const r = rIdx + 1;
|