@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExamQuestionsSdkApi } from '../../api/exam-questions.types';
|
|
2
|
+
import { ExamPreviewMode } from './exam-preview.mode';
|
|
2
3
|
export interface ExamPreviewDialogProps {
|
|
3
4
|
open: boolean;
|
|
4
5
|
onOpenChange: (open: boolean) => void;
|
|
@@ -8,12 +9,17 @@ export interface ExamPreviewDialogProps {
|
|
|
8
9
|
api: ExamQuestionsSdkApi;
|
|
9
10
|
/** Fallback heading shown until the exam name loads. */
|
|
10
11
|
title?: string;
|
|
12
|
+
/** Initial preview mode. Default is student view without answers. */
|
|
13
|
+
defaultMode?: ExamPreviewMode;
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
13
16
|
* Read-only popup that previews an exam by id.
|
|
14
17
|
*
|
|
18
|
+
* Supports two modes:
|
|
19
|
+
* - 'student' (default): interactive student taking view without revealing answers
|
|
20
|
+
* - 'answers': review view with correct answers highlighted and explanations
|
|
21
|
+
*
|
|
15
22
|
* UI mirrors the host app: one Part per screen (all questions of that part),
|
|
16
|
-
* Next/Prev navigates between parts.
|
|
17
|
-
* {@link ExamQuestionsSdkApi}. Questions render via QuestionViewer in review mode.
|
|
23
|
+
* Next/Prev navigates between parts. Data comes from {@link ExamQuestionsSdkApi}.
|
|
18
24
|
*/
|
|
19
|
-
export declare function ExamPreviewDialog({ open, onOpenChange, examId, api, title, }: ExamPreviewDialogProps): import("react").JSX.Element;
|
|
25
|
+
export declare function ExamPreviewDialog({ open, onOpenChange, examId, api, title, defaultMode, }: ExamPreviewDialogProps): import("react").JSX.Element;
|
|
@@ -1,30 +1,42 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { Eye } from 'lucide-react';
|
|
4
|
+
import { CheckCircle2, Eye, GraduationCap } from 'lucide-react';
|
|
5
5
|
import { Button } from '../../components/ui/button';
|
|
6
6
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../components/ui/dialog';
|
|
7
7
|
import { useT } from '../../shared/lib/i18n';
|
|
8
|
+
import { cn } from '../../shared/lib/utils';
|
|
9
|
+
import { ExamPreviewMode } from './exam-preview.mode';
|
|
8
10
|
import { ExamPreviewPartContent } from './ExamPreviewPartContent';
|
|
9
11
|
import { ExamPreviewSidebar } from './ExamPreviewSidebar';
|
|
10
12
|
import { getQuestionNumber, getQuestionPoints, groupQuestionsByPart, sumQuestionPoints, unwrapApiData, } from './exam-preview.types';
|
|
11
13
|
/**
|
|
12
14
|
* Read-only popup that previews an exam by id.
|
|
13
15
|
*
|
|
16
|
+
* Supports two modes:
|
|
17
|
+
* - 'student' (default): interactive student taking view without revealing answers
|
|
18
|
+
* - 'answers': review view with correct answers highlighted and explanations
|
|
19
|
+
*
|
|
14
20
|
* UI mirrors the host app: one Part per screen (all questions of that part),
|
|
15
|
-
* Next/Prev navigates between parts.
|
|
16
|
-
* {@link ExamQuestionsSdkApi}. Questions render via QuestionViewer in review mode.
|
|
21
|
+
* Next/Prev navigates between parts. Data comes from {@link ExamQuestionsSdkApi}.
|
|
17
22
|
*/
|
|
18
|
-
export function ExamPreviewDialog({ open, onOpenChange, examId, api, title, }) {
|
|
23
|
+
export function ExamPreviewDialog({ open, onOpenChange, examId, api, title, defaultMode = ExamPreviewMode.STUDENT, }) {
|
|
19
24
|
const t = useT();
|
|
20
25
|
const [exam, setExam] = useState(null);
|
|
21
26
|
const [questions, setQuestions] = useState([]);
|
|
22
27
|
const [currentPartIndex, setCurrentPartIndex] = useState(0);
|
|
23
28
|
const [showPartsDetail, setShowPartsDetail] = useState(false);
|
|
29
|
+
const [previewMode, setPreviewMode] = useState(defaultMode);
|
|
24
30
|
const [loading, setLoading] = useState(false);
|
|
25
31
|
const [error, setError] = useState(null);
|
|
26
32
|
const [reloadKey, setReloadKey] = useState(0);
|
|
27
33
|
const contentRef = useRef(null);
|
|
34
|
+
// Sync / reset previewMode whenever dialog opens
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (open) {
|
|
37
|
+
setPreviewMode(defaultMode);
|
|
38
|
+
}
|
|
39
|
+
}, [open, defaultMode]);
|
|
28
40
|
// Fetch exam + questions whenever the dialog opens for an exam.
|
|
29
41
|
useEffect(() => {
|
|
30
42
|
if (!open || !examId) {
|
|
@@ -117,5 +129,11 @@ export function ExamPreviewDialog({ open, onOpenChange, examId, api, title, }) {
|
|
|
117
129
|
scrollToTop();
|
|
118
130
|
}
|
|
119
131
|
};
|
|
120
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-h-[95vh] max-w-7xl overflow-y-auto", children: [
|
|
132
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-h-[95vh] max-w-7xl overflow-y-auto", children: [_jsxs(DialogHeader, { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between border-b pb-3 pr-8", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2 text-lg font-bold", children: [_jsx(Eye, { className: "h-5 w-5 text-blue-600" }), t('admin.exams.preview.title', { name: heading })] }), _jsxs("div", { className: "inline-flex items-center rounded-lg border border-gray-200 bg-gray-100 p-1 shadow-inner", children: [_jsxs("button", { type: "button", onClick: () => setPreviewMode(ExamPreviewMode.STUDENT), className: cn('inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-semibold transition-all', previewMode === ExamPreviewMode.STUDENT
|
|
133
|
+
? 'bg-white text-blue-700 shadow-sm ring-1 ring-black/5'
|
|
134
|
+
: 'text-gray-600 hover:bg-gray-200/70 hover:text-gray-900'), children: [_jsx(GraduationCap, { className: "h-4 w-4" }), _jsx("span", { children: t('admin.exams.preview.mode.student') })] }), _jsxs("button", { type: "button", onClick: () => setPreviewMode(ExamPreviewMode.ANSWERS), className: cn('inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-semibold transition-all', previewMode === ExamPreviewMode.ANSWERS
|
|
135
|
+
? 'bg-emerald-600 text-white shadow-sm'
|
|
136
|
+
: 'text-gray-600 hover:bg-gray-200/70 hover:text-gray-900'), children: [_jsx(CheckCircle2, { className: "h-4 w-4" }), _jsx("span", { children: t('admin.exams.preview.mode.answers') })] })] })] }), _jsxs("div", { className: "flex h-[80vh] flex-col gap-4 sm:flex-row", children: [_jsx(ExamPreviewSidebar, { examName: heading, duration: duration, totalPoints: totalPoints, questions: questions, questionsByPart: questionsByPart, currentPart: currentPart, currentPartIndex: currentPartIndex, totalParts: totalParts, progress: progress, loading: loading, showPartsDetail: showPartsDetail, previewMode: previewMode, onTogglePartsDetail: () => setShowPartsDetail((value) => !value), onGoToPart: goToPart }), _jsx(ExamPreviewPartContent, { loading: loading, error: error, questions: questions, currentPart: currentPart, currentPartQuestions: currentPartQuestions, currentPartIndex: currentPartIndex, totalParts: totalParts, partDisplayName: partDisplayName, partTotalPoints: partTotalPoints, contentRef: contentRef, isReviewMode: previewMode === ExamPreviewMode.ANSWERS, onRetry: () => setReloadKey((key) => key + 1), onPreviousPart: goToPreviousPart, onNextPart: goToNextPart })] }), _jsx("div", { className: "border-t pt-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-sm text-gray-600", children: previewMode === ExamPreviewMode.ANSWERS
|
|
137
|
+
? t('admin.exams.preview.footerNoteAnswers')
|
|
138
|
+
: t('admin.exams.preview.footerNoteStudent') }), _jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), className: "border-gray-300", children: t('admin.exams.preview.close') })] }) })] }) }));
|
|
121
139
|
}
|
|
@@ -11,8 +11,9 @@ export interface ExamPreviewPartContentProps {
|
|
|
11
11
|
partDisplayName: string;
|
|
12
12
|
partTotalPoints: number;
|
|
13
13
|
contentRef: RefObject<HTMLDivElement | null>;
|
|
14
|
+
isReviewMode?: boolean;
|
|
14
15
|
onRetry: () => void;
|
|
15
16
|
onPreviousPart: () => void;
|
|
16
17
|
onNextPart: () => void;
|
|
17
18
|
}
|
|
18
|
-
export declare function ExamPreviewPartContent({ loading, error, questions, currentPart, currentPartQuestions, currentPartIndex, totalParts, partDisplayName, partTotalPoints, contentRef, onRetry, onPreviousPart, onNextPart, }: ExamPreviewPartContentProps): import("react").JSX.Element;
|
|
19
|
+
export declare function ExamPreviewPartContent({ loading, error, questions, currentPart, currentPartQuestions, currentPartIndex, totalParts, partDisplayName, partTotalPoints, contentRef, isReviewMode, onRetry, onPreviousPart, onNextPart, }: ExamPreviewPartContentProps): import("react").JSX.Element;
|
|
@@ -5,8 +5,9 @@ import { Badge } from '../../components/ui/badge';
|
|
|
5
5
|
import { Button } from '../../components/ui/button';
|
|
6
6
|
import { QuestionViewer } from '../questions/viewer/QuestionViewer';
|
|
7
7
|
import { useT } from '../../shared/lib/i18n';
|
|
8
|
+
import { ExamPreviewMode } from './exam-preview.mode';
|
|
8
9
|
import { difficultyBadgeClass, getQuestionDifficulty, getQuestionNumber, getQuestionPoints, } from './exam-preview.types';
|
|
9
|
-
export function ExamPreviewPartContent({ loading, error, questions, currentPart, currentPartQuestions, currentPartIndex, totalParts, partDisplayName, partTotalPoints, contentRef, onRetry, onPreviousPart, onNextPart, }) {
|
|
10
|
+
export function ExamPreviewPartContent({ loading, error, questions, currentPart, currentPartQuestions, currentPartIndex, totalParts, partDisplayName, partTotalPoints, contentRef, isReviewMode = false, onRetry, onPreviousPart, onNextPart, }) {
|
|
10
11
|
const t = useT();
|
|
11
12
|
return (_jsx("div", { className: "flex-1 overflow-y-auto", children: _jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center justify-between border-b pb-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold", children: loading ? (_jsx("span", { className: "text-gray-400", children: t('admin.exams.preview.loading') })) : totalParts > 0 ? (t('admin.exams.preview.partIndex', {
|
|
12
13
|
index: String(currentPartIndex + 1),
|
|
@@ -22,6 +23,6 @@ export function ExamPreviewPartContent({ loading, error, questions, currentPart,
|
|
|
22
23
|
number: String(qNumber),
|
|
23
24
|
}) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: "outline", children: t('admin.exams.preview.points', {
|
|
24
25
|
count: String(getQuestionPoints(question)),
|
|
25
|
-
}) }), _jsx(Badge, { className: difficultyBadgeClass(difficulty), children: difficulty })] })] }), _jsx(QuestionViewer, { question: question, isReviewMode:
|
|
26
|
+
}) }), _jsx(Badge, { className: difficultyBadgeClass(difficulty), children: difficulty })] })] }), _jsx(QuestionViewer, { question: question, isReviewMode: isReviewMode }, `${question.id}-${isReviewMode ? ExamPreviewMode.ANSWERS : ExamPreviewMode.STUDENT}`)] }, question.id));
|
|
26
27
|
}) })) : null }), !loading && totalParts > 0 && (_jsxs("div", { className: "mt-auto flex items-center justify-between border-t border-gray-200 bg-gray-50 px-4 py-3", children: [_jsxs(Button, { variant: "outline", onClick: onPreviousPart, disabled: currentPartIndex === 0, className: "flex h-9 items-center gap-2 px-3 text-sm", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('admin.exams.preview.prevPart') }), _jsx("span", { className: "sm:hidden", children: t('admin.exams.preview.prev') })] }), _jsxs("div", { className: "text-sm text-gray-600", children: [currentPartIndex + 1, " / ", totalParts] }), _jsxs(Button, { variant: "outline", onClick: onNextPart, disabled: currentPartIndex === totalParts - 1, className: "flex h-9 items-center gap-2 px-3 text-sm", children: [_jsx("span", { className: "hidden sm:inline", children: t('admin.exams.preview.nextPart') }), _jsx("span", { className: "sm:hidden", children: t('admin.exams.preview.next') }), _jsx(ArrowRight, { className: "h-4 w-4" })] })] }))] }) }));
|
|
27
28
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ExamPreviewMode } from './exam-preview.mode';
|
|
1
2
|
import { type PreviewQuestion, type QuestionsByPart } from './exam-preview.types';
|
|
2
3
|
export interface ExamPreviewSidebarProps {
|
|
3
4
|
examName: string;
|
|
@@ -11,7 +12,8 @@ export interface ExamPreviewSidebarProps {
|
|
|
11
12
|
progress: number;
|
|
12
13
|
loading: boolean;
|
|
13
14
|
showPartsDetail: boolean;
|
|
15
|
+
previewMode?: ExamPreviewMode;
|
|
14
16
|
onTogglePartsDetail: () => void;
|
|
15
17
|
onGoToPart: (index: number) => void;
|
|
16
18
|
}
|
|
17
|
-
export declare function ExamPreviewSidebar({ examName, duration, totalPoints, questions, questionsByPart, currentPart, currentPartIndex, totalParts, progress, loading, showPartsDetail, onTogglePartsDetail, onGoToPart, }: ExamPreviewSidebarProps): import("react").JSX.Element;
|
|
19
|
+
export declare function ExamPreviewSidebar({ examName, duration, totalPoints, questions, questionsByPart, currentPart, currentPartIndex, totalParts, progress, loading, showPartsDetail, previewMode, onTogglePartsDetail, onGoToPart, }: ExamPreviewSidebarProps): import("react").JSX.Element;
|
|
@@ -6,10 +6,15 @@ import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/ca
|
|
|
6
6
|
import { Progress } from '../../components/ui/progress';
|
|
7
7
|
import { Loader2 } from 'lucide-react';
|
|
8
8
|
import { useT } from '../../shared/lib/i18n';
|
|
9
|
+
import { ExamPreviewMode } from './exam-preview.mode';
|
|
9
10
|
import { getQuestionNumber, } from './exam-preview.types';
|
|
10
|
-
export function ExamPreviewSidebar({ examName, duration, totalPoints, questions, questionsByPart, currentPart, currentPartIndex, totalParts, progress, loading, showPartsDetail, onTogglePartsDetail, onGoToPart, }) {
|
|
11
|
+
export function ExamPreviewSidebar({ examName, duration, totalPoints, questions, questionsByPart, currentPart, currentPartIndex, totalParts, progress, loading, showPartsDetail, previewMode = ExamPreviewMode.STUDENT, onTogglePartsDetail, onGoToPart, }) {
|
|
11
12
|
const t = useT();
|
|
12
|
-
return (_jsx("div", { className: "w-full overflow-y-auto border-b border-gray-200 sm:w-80 sm:border-b-0 sm:border-r", children: _jsxs("div", { className: "space-y-4 p-4", children: [_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "text-base", children: t('admin.exams.preview.examInfo') }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-gray-600", children: t('admin.exams.preview.
|
|
13
|
+
return (_jsx("div", { className: "w-full overflow-y-auto border-b border-gray-200 sm:w-80 sm:border-b-0 sm:border-r", children: _jsxs("div", { className: "space-y-4 p-4", children: [_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "text-base", children: t('admin.exams.preview.examInfo') }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-gray-600", children: t('admin.exams.preview.mode.label') }), _jsx(Badge, { variant: "outline", className: previewMode === ExamPreviewMode.ANSWERS
|
|
14
|
+
? 'border-emerald-300 bg-emerald-50 text-emerald-700 font-semibold'
|
|
15
|
+
: 'border-blue-300 bg-blue-50 text-blue-700 font-semibold', children: previewMode === ExamPreviewMode.ANSWERS
|
|
16
|
+
? t('admin.exams.preview.mode.answers')
|
|
17
|
+
: t('admin.exams.preview.mode.student') })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-gray-600", children: t('admin.exams.preview.duration') }), _jsx("span", { className: "text-sm font-medium", children: t('admin.exams.view.minutes', { count: String(duration) }) })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-gray-600", children: t('admin.exams.preview.questionCount') }), _jsx("span", { className: "text-sm font-medium", children: loading
|
|
13
18
|
? '...'
|
|
14
19
|
: t('admin.exams.preview.questions', { count: String(questions.length) }) })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-gray-600", children: t('admin.exams.preview.totalPoints') }), _jsx("span", { className: "text-sm font-medium", children: t('admin.exams.preview.points', { count: String(totalPoints) }) })] })] })] }), !loading && totalParts > 0 && (_jsx(Card, { children: _jsx(CardContent, { className: "p-4", children: _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex justify-between text-sm", children: [_jsx("span", { children: t('admin.exams.preview.progress') }), _jsxs("span", { className: "font-medium", children: [currentPartIndex + 1, "/", totalParts] })] }), _jsx(Progress, { value: progress, className: "h-2" })] }) }) })), loading ? (_jsx("div", { className: "flex items-center justify-center py-4", children: _jsxs("div", { className: "flex items-center gap-2 text-gray-500", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), _jsx("span", { className: "text-sm", children: t('admin.exams.preview.loading') })] }) })) : (totalParts > 0 && (_jsxs("div", { className: "space-y-3", children: [_jsx("h4", { className: "text-sm font-medium text-gray-700", children: t('admin.exams.preview.questionNavigation') }), _jsx("div", { className: "flex flex-wrap gap-2", children: questionsByPart.map((part, index) => {
|
|
15
20
|
const isCurrent = index === currentPartIndex;
|
|
@@ -32,5 +32,6 @@ export { EXAM_DIFFICULTY, ExamViewDialog } from './ExamViewDialog';
|
|
|
32
32
|
export type { ExamDifficulty, ExamViewDialogExam, ExamViewDialogProps } from './ExamViewDialog';
|
|
33
33
|
export { ExamPreviewDialog } from './ExamPreviewDialog';
|
|
34
34
|
export type { ExamPreviewDialogProps } from './ExamPreviewDialog';
|
|
35
|
+
export { ExamPreviewMode } from './exam-preview.mode';
|
|
35
36
|
export type { PreviewQuestion, QuestionsByPart } from './exam-preview.types';
|
|
36
37
|
export { QuestionStatus, clearQuestionStatuses, getQuestionStatus, initializeQuestionStatusesFromAPI, setQuestionStatus } from './exam-question-status';
|
|
@@ -16,4 +16,5 @@ export { UnsavedChangesDialog } from './UnsavedChangesDialog';
|
|
|
16
16
|
export { CompletionDialog } from './CompletionDialog';
|
|
17
17
|
export { EXAM_DIFFICULTY, ExamViewDialog } from './ExamViewDialog';
|
|
18
18
|
export { ExamPreviewDialog } from './ExamPreviewDialog';
|
|
19
|
+
export { ExamPreviewMode } from './exam-preview.mode';
|
|
19
20
|
export { QuestionStatus, clearQuestionStatuses, getQuestionStatus, initializeQuestionStatusesFromAPI, setQuestionStatus } from './exam-question-status';
|
|
@@ -8,9 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* 2. Transforms data to renderer-specific props
|
|
9
9
|
* 3. Renders the correct component for each question type
|
|
10
10
|
*/
|
|
11
|
-
import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, } from './question-renderers';
|
|
11
|
+
import { MoversListenAndWriteRenderer, MoversChooseBestAnswerRenderer, MoversChooseAdjectiveRenderer, MoversWritingRenderer, MoversColoringRenderer, MoversPictureChooseRenderer, MoversPictureFillBlankChooseRenderer, MoversReadingPassageRenderer, MoversLabelThePictureRenderer, MoversFillWordHintRenderer, MoversGridFillRenderer, MoversTrueFalseRenderer, MoversCrossOutWordGroupRenderer, MoversAnswerTheQuestionRenderer, MoversWriteSentencesRenderer, MoversWordOrderingRenderer, MoversWordFillParagraphRenderer, MoversMatchByWritingAnswerRenderer, MoversWordFillStructuredFormRenderer, MoversWordOrderAndMatchGroupRenderer, MoversCrosswordPuzzleRenderer, } from './question-renderers';
|
|
12
12
|
import { SpeakingQuestionRenderer } from './speaking/renderers';
|
|
13
|
-
import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformTrueFalse, transformTrueFalseGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, } from '../utils/question-transformers';
|
|
13
|
+
import { transformFillInBlank, transformWriteCorrectVerbForm, transformChooseCorrectAnswer, transformChooseTheCorrectAnswerGroup, transformChooseCorrectAdjective, transformWriteShortLetter, transformColorObjects, transformPictureChoose, transformPictureFillBlankChoose, transformReadingPassage, transformLabelPicture, transformFillWordHint, transformGridFill, transformAnswerTheQuestion, transformAnswerTheQuestionGroup, transformTrueFalse, transformTrueFalseGroup, transformWordOrdering, transformWordFillParagraph, transformMatchByWritingAnswer, transformWriteSentences, transformWordFillStructuredForm, transformWordOrderAndMatchGroup, transformCrosswordPuzzle, } from '../utils/question-transformers';
|
|
14
14
|
import { transformCrossOutWordGroup } from '../utils/cross-out-word-group.transformer';
|
|
15
15
|
import { normalizeColoringAssignments } from '../utils/coloring-assignments';
|
|
16
16
|
export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode = false, onExit, onPartComplete, onPartBack, speakingTheme, initialQuestionIndex, skipTeacherVideo, onTeacherIntroPlayed, currentQuestionIndex, }) {
|
|
@@ -112,6 +112,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
112
112
|
const data = transformGridFill(questions);
|
|
113
113
|
return (_jsx(MoversGridFillRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, grid: data.grid, rows: data.rows, columns: data.columns, cellAnswers: {}, answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode, disabledCells: data.disabledCells }));
|
|
114
114
|
}
|
|
115
|
+
case 'CROSSWORD_PUZZLE': {
|
|
116
|
+
const data = transformCrosswordPuzzle(questions);
|
|
117
|
+
return (_jsx(MoversCrosswordPuzzleRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, title: data.title, gridSize: data.gridSize, clues: data.clues, answers: answers, correctAnswers: data.correctAnswers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
118
|
+
}
|
|
115
119
|
case 'IMAGE_OBJECT_MATCHING': {
|
|
116
120
|
// Reuse coloring renderer — same content structure as READ_AND_COLOR_OBJECTS
|
|
117
121
|
const data = transformColorObjects(questions);
|
|
@@ -137,6 +141,10 @@ export function QuestionRenderer({ part, answers, onAnswerChange, isReviewMode =
|
|
|
137
141
|
const data = transformAnswerTheQuestion(questions);
|
|
138
142
|
return (_jsx(MoversAnswerTheQuestionRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, audioUrl: part.audioUrl, title: data.title, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
139
143
|
}
|
|
144
|
+
case 'ANSWER_THE_QUESTION_GROUP': {
|
|
145
|
+
const data = transformAnswerTheQuestionGroup(questions);
|
|
146
|
+
return (_jsx(MoversAnswerTheQuestionRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
|
147
|
+
}
|
|
140
148
|
case 'TRUE_FALSE_GROUP': {
|
|
141
149
|
const data = transformTrueFalseGroup(questions);
|
|
142
150
|
return (_jsx(MoversTrueFalseRenderer, { partNumber: partNo, partName: part.name, questionCount: questionCount, instruction: part.instructions || data.instruction, imageUrl: data.imageUrl, groupContent: data.groupContent, questions: getRenderQuestions(data.questions), answers: answers, onAnswerChange: onAnswerChange, isReviewMode: isReviewMode }));
|
package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { MoversCrosswordPuzzleRendererProps } from '../../types';
|
|
3
|
+
export declare function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCount, instruction, title, gridSize, clues, answers, correctAnswers, onAnswerChange, isReviewMode, caseSensitive, }: MoversCrosswordPuzzleRendererProps): React.JSX.Element;
|
package/dist/components/exams/take/components/question-renderers/MoversCrosswordPuzzleRenderer.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useMemo, useRef, useEffect } from 'react';
|
|
4
|
+
import { Check, X } from 'lucide-react';
|
|
5
|
+
import CambridgeYlePartBanner from '../../../../themes/cambridge-yle/CambridgeYlePartBanner';
|
|
6
|
+
import CambridgeYleInstructionBanner from '../../../../themes/cambridge-yle/CambridgeYleInstructionBanner';
|
|
7
|
+
export function MoversCrosswordPuzzleRenderer({ partNumber, partName, questionCount, instruction, title, gridSize, clues, answers = {}, correctAnswers = {}, onAnswerChange, isReviewMode = false, caseSensitive = false, }) {
|
|
8
|
+
const rows = gridSize?.rows || 12;
|
|
9
|
+
const cols = gridSize?.cols || 12;
|
|
10
|
+
const acrossClues = useMemo(() => clues?.across || [], [clues?.across]);
|
|
11
|
+
const downClues = useMemo(() => clues?.down || [], [clues?.down]);
|
|
12
|
+
const [activeDirection, setActiveDirection] = useState('across');
|
|
13
|
+
const [activeClueNumber, setActiveClueNumber] = useState(() => {
|
|
14
|
+
return acrossClues[0]?.number || downClues[0]?.number || 1;
|
|
15
|
+
});
|
|
16
|
+
const [selectedCell, setSelectedCell] = useState(null);
|
|
17
|
+
const inputRefs = useRef(new Map());
|
|
18
|
+
const { cellMap, clueNumberMap, wordCellsMap } = useMemo(() => {
|
|
19
|
+
const cMap = new Map();
|
|
20
|
+
const numMap = new Map();
|
|
21
|
+
const wMap = new Map();
|
|
22
|
+
for (const c of acrossClues) {
|
|
23
|
+
numMap.set(`${c.startRow}-${c.startCol}`, c.number);
|
|
24
|
+
const cells = [];
|
|
25
|
+
for (let i = 0; i < c.length; i++) {
|
|
26
|
+
const key = `${c.startRow}-${c.startCol + i}`;
|
|
27
|
+
cells.push({ r: c.startRow, c: c.startCol + i });
|
|
28
|
+
const existing = cMap.get(key) || {};
|
|
29
|
+
cMap.set(key, { ...existing, across: c });
|
|
30
|
+
}
|
|
31
|
+
wMap.set(`across-${c.number}`, cells);
|
|
32
|
+
}
|
|
33
|
+
for (const c of downClues) {
|
|
34
|
+
if (!numMap.has(`${c.startRow}-${c.startCol}`)) {
|
|
35
|
+
numMap.set(`${c.startRow}-${c.startCol}`, c.number);
|
|
36
|
+
}
|
|
37
|
+
const cells = [];
|
|
38
|
+
for (let i = 0; i < c.length; i++) {
|
|
39
|
+
const key = `${c.startRow + i}-${c.startCol}`;
|
|
40
|
+
cells.push({ r: c.startRow + i, c: c.startCol });
|
|
41
|
+
const existing = cMap.get(key) || {};
|
|
42
|
+
cMap.set(key, { ...existing, down: c });
|
|
43
|
+
}
|
|
44
|
+
wMap.set(`down-${c.number}`, cells);
|
|
45
|
+
}
|
|
46
|
+
return { cellMap: cMap, clueNumberMap: numMap, wordCellsMap: wMap };
|
|
47
|
+
}, [acrossClues, downClues]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!selectedCell) {
|
|
50
|
+
const firstClue = acrossClues[0] || downClues[0];
|
|
51
|
+
if (firstClue) {
|
|
52
|
+
setSelectedCell({ r: firstClue.startRow, c: firstClue.startCol });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}, [acrossClues, downClues, selectedCell]);
|
|
56
|
+
const activeWordCells = useMemo(() => {
|
|
57
|
+
return wordCellsMap.get(`${activeDirection}-${activeClueNumber}`) || [];
|
|
58
|
+
}, [wordCellsMap, activeDirection, activeClueNumber]);
|
|
59
|
+
const handleSelectClue = (number, direction) => {
|
|
60
|
+
setActiveDirection(direction);
|
|
61
|
+
setActiveClueNumber(number);
|
|
62
|
+
const clueList = direction === 'across' ? acrossClues : downClues;
|
|
63
|
+
const clue = clueList.find((c) => c.number === number);
|
|
64
|
+
if (clue) {
|
|
65
|
+
setSelectedCell({ r: clue.startRow, c: clue.startCol });
|
|
66
|
+
const key = `${clue.startRow}-${clue.startCol}`;
|
|
67
|
+
inputRefs.current.get(key)?.focus();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const handleCellClick = (r, c) => {
|
|
71
|
+
const key = `${r}-${c}`;
|
|
72
|
+
const cellInfo = cellMap.get(key);
|
|
73
|
+
if (!cellInfo)
|
|
74
|
+
return;
|
|
75
|
+
if (selectedCell?.r === r && selectedCell?.c === c && cellInfo.across && cellInfo.down) {
|
|
76
|
+
const newDir = activeDirection === 'across' ? 'down' : 'across';
|
|
77
|
+
setActiveDirection(newDir);
|
|
78
|
+
const newClue = newDir === 'across' ? cellInfo.across : cellInfo.down;
|
|
79
|
+
if (newClue)
|
|
80
|
+
setActiveClueNumber(newClue.number);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
if (activeDirection === 'across' && cellInfo.across) {
|
|
84
|
+
setActiveClueNumber(cellInfo.across.number);
|
|
85
|
+
}
|
|
86
|
+
else if (activeDirection === 'down' && cellInfo.down) {
|
|
87
|
+
setActiveClueNumber(cellInfo.down.number);
|
|
88
|
+
}
|
|
89
|
+
else if (cellInfo.across) {
|
|
90
|
+
setActiveDirection('across');
|
|
91
|
+
setActiveClueNumber(cellInfo.across.number);
|
|
92
|
+
}
|
|
93
|
+
else if (cellInfo.down) {
|
|
94
|
+
setActiveDirection('down');
|
|
95
|
+
setActiveClueNumber(cellInfo.down.number);
|
|
96
|
+
}
|
|
97
|
+
setSelectedCell({ r, c });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const moveToNextCell = (r, c) => {
|
|
101
|
+
const cells = activeWordCells;
|
|
102
|
+
const currIdx = cells.findIndex((cell) => cell.r === r && cell.c === c);
|
|
103
|
+
if (currIdx >= 0 && currIdx < cells.length - 1) {
|
|
104
|
+
const next = cells[currIdx + 1];
|
|
105
|
+
setSelectedCell(next);
|
|
106
|
+
inputRefs.current.get(`${next.r}-${next.c}`)?.focus();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const moveToPrevCell = (r, c) => {
|
|
110
|
+
const cells = activeWordCells;
|
|
111
|
+
const currIdx = cells.findIndex((cell) => cell.r === r && cell.c === c);
|
|
112
|
+
if (currIdx > 0) {
|
|
113
|
+
const prev = cells[currIdx - 1];
|
|
114
|
+
setSelectedCell(prev);
|
|
115
|
+
inputRefs.current.get(`${prev.r}-${prev.c}`)?.focus();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const handleKeyDown = (e, r, c) => {
|
|
119
|
+
if (isReviewMode)
|
|
120
|
+
return;
|
|
121
|
+
if (e.key === 'Backspace') {
|
|
122
|
+
const key = `${r}-${c}`;
|
|
123
|
+
if (!answers[key]) {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
moveToPrevCell(r, c);
|
|
126
|
+
const cells = activeWordCells;
|
|
127
|
+
const currIdx = cells.findIndex((cell) => cell.r === r && cell.c === c);
|
|
128
|
+
if (currIdx > 0) {
|
|
129
|
+
const prev = cells[currIdx - 1];
|
|
130
|
+
onAnswerChange(`${prev.r}-${prev.c}`, '');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
onAnswerChange(key, '');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (e.key === 'ArrowRight') {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
if (activeDirection !== 'across') {
|
|
140
|
+
setActiveDirection('across');
|
|
141
|
+
const info = cellMap.get(`${r}-${c}`);
|
|
142
|
+
if (info?.across)
|
|
143
|
+
setActiveClueNumber(info.across.number);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
moveToNextCell(r, c);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (e.key === 'ArrowLeft') {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
if (activeDirection !== 'across') {
|
|
152
|
+
setActiveDirection('across');
|
|
153
|
+
const info = cellMap.get(`${r}-${c}`);
|
|
154
|
+
if (info?.across)
|
|
155
|
+
setActiveClueNumber(info.across.number);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
moveToPrevCell(r, c);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (e.key === 'ArrowDown') {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
if (activeDirection !== 'down') {
|
|
164
|
+
setActiveDirection('down');
|
|
165
|
+
const info = cellMap.get(`${r}-${c}`);
|
|
166
|
+
if (info?.down)
|
|
167
|
+
setActiveClueNumber(info.down.number);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
moveToNextCell(r, c);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (e.key === 'ArrowUp') {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
if (activeDirection !== 'down') {
|
|
176
|
+
setActiveDirection('down');
|
|
177
|
+
const info = cellMap.get(`${r}-${c}`);
|
|
178
|
+
if (info?.down)
|
|
179
|
+
setActiveClueNumber(info.down.number);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
moveToPrevCell(r, c);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const handleInputChange = (val, r, c) => {
|
|
187
|
+
if (isReviewMode)
|
|
188
|
+
return;
|
|
189
|
+
const cleanVal = val.slice(-1).toUpperCase();
|
|
190
|
+
const key = `${r}-${c}`;
|
|
191
|
+
onAnswerChange(key, cleanVal);
|
|
192
|
+
if (cleanVal) {
|
|
193
|
+
moveToNextCell(r, c);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const getCellReviewStatus = (r, c) => {
|
|
197
|
+
if (!isReviewMode)
|
|
198
|
+
return null;
|
|
199
|
+
const key = `${r}-${c}`;
|
|
200
|
+
const userLetter = (answers[key] || '').trim();
|
|
201
|
+
const cellInfo = cellMap.get(key);
|
|
202
|
+
if (!cellInfo)
|
|
203
|
+
return null;
|
|
204
|
+
let expectedLetter = '';
|
|
205
|
+
if (cellInfo.across && correctAnswers[`across-${cellInfo.across.number}`]) {
|
|
206
|
+
const word = correctAnswers[`across-${cellInfo.across.number}`];
|
|
207
|
+
const offset = c - cellInfo.across.startCol;
|
|
208
|
+
expectedLetter = word[offset] || '';
|
|
209
|
+
}
|
|
210
|
+
else if (cellInfo.down && correctAnswers[`down-${cellInfo.down.number}`]) {
|
|
211
|
+
const word = correctAnswers[`down-${cellInfo.down.number}`];
|
|
212
|
+
const offset = r - cellInfo.down.startRow;
|
|
213
|
+
expectedLetter = word[offset] || '';
|
|
214
|
+
}
|
|
215
|
+
if (!expectedLetter)
|
|
216
|
+
return null;
|
|
217
|
+
const isMatch = caseSensitive
|
|
218
|
+
? userLetter === expectedLetter
|
|
219
|
+
: userLetter.toLowerCase() === expectedLetter.toLowerCase();
|
|
220
|
+
return { isCorrect: isMatch, expected: expectedLetter, user: userLetter };
|
|
221
|
+
};
|
|
222
|
+
return (_jsxs("div", { className: "flex h-full flex-col gap-4 overflow-y-auto p-3 sm:gap-5 sm:p-4 md:gap-6 md:p-6", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(CambridgeYlePartBanner, { partNumber: partNumber, questionCount: questionCount, partName: partName, isReviewMode: isReviewMode }), _jsx(CambridgeYleInstructionBanner, { instruction: instruction, isReviewMode: isReviewMode })] }), title && (_jsx("h3", { className: "text-center text-lg font-bold text-gray-800 tracking-wide", children: 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-xl border-2 border-blue-200 bg-blue-50/40 p-4 shadow-sm", children: [_jsxs("h4", { className: "font-bold text-blue-900 mb-3 flex items-center justify-between", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-blue-600 text-white text-xs font-bold uppercase", children: "Across" }), _jsx("span", { children: "H\u00E0ng ngang" })] }), _jsxs("span", { className: "text-xs text-blue-600 font-medium", children: ["(", acrossClues.length, " c\u00E2u)"] })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-72 overflow-y-auto pr-1", children: acrossClues.map((c) => {
|
|
223
|
+
const isActive = activeDirection === 'across' && activeClueNumber === c.number;
|
|
224
|
+
const answerKey = `across-${c.number}`;
|
|
225
|
+
const expectedWord = correctAnswers[answerKey];
|
|
226
|
+
let userWord = '';
|
|
227
|
+
for (let i = 0; i < c.length; i++) {
|
|
228
|
+
userWord += (answers[`${c.startRow}-${c.startCol + i}`] || '').trim();
|
|
229
|
+
}
|
|
230
|
+
const isFilled = userWord.length === c.length;
|
|
231
|
+
return (_jsxs("button", { type: "button", onClick: () => handleSelectClue(c.number, 'across'), className: `flex items-start gap-2.5 p-2.5 rounded-lg border text-left transition ${isActive
|
|
232
|
+
? 'border-blue-600 bg-blue-100/90 shadow-sm ring-1 ring-blue-500'
|
|
233
|
+
: 'border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50/60'}`, children: [_jsx("span", { className: `flex items-center justify-center h-6 w-6 rounded-md text-xs font-bold ${isActive ? 'bg-blue-600 text-white' : 'bg-blue-100 text-blue-800'}`, children: c.number }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: c.clue }), isReviewMode && expectedWord && (_jsxs("div", { className: "text-xs mt-1 flex items-center gap-2", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "font-bold text-green-700", children: expectedWord }), userWord && userWord !== expectedWord && (_jsx("span", { className: "line-through text-red-500 font-semibold", children: userWord }))] }))] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("span", { className: "text-xs text-gray-400 font-medium", children: ["(", c.length, ")"] }), isFilled && !isReviewMode && (_jsx("span", { className: "h-2 w-2 rounded-full bg-blue-500" }))] })] }, `across-${c.number}`));
|
|
234
|
+
}) })] }), _jsxs("div", { className: "rounded-xl border-2 border-purple-200 bg-purple-50/40 p-4 shadow-sm", children: [_jsxs("h4", { className: "font-bold text-purple-900 mb-3 flex items-center justify-between", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { className: "px-2 py-0.5 rounded bg-purple-600 text-white text-xs font-bold uppercase", children: "Down" }), _jsx("span", { children: "H\u00E0ng d\u1ECDc" })] }), _jsxs("span", { className: "text-xs text-purple-600 font-medium", children: ["(", downClues.length, " c\u00E2u)"] })] }), _jsx("div", { className: "flex flex-col gap-2 max-h-72 overflow-y-auto pr-1", children: downClues.map((c) => {
|
|
235
|
+
const isActive = activeDirection === 'down' && activeClueNumber === c.number;
|
|
236
|
+
const answerKey = `down-${c.number}`;
|
|
237
|
+
const expectedWord = correctAnswers[answerKey];
|
|
238
|
+
let userWord = '';
|
|
239
|
+
for (let i = 0; i < c.length; i++) {
|
|
240
|
+
userWord += (answers[`${c.startRow + i}-${c.startCol}`] || '').trim();
|
|
241
|
+
}
|
|
242
|
+
const isFilled = userWord.length === c.length;
|
|
243
|
+
return (_jsxs("button", { type: "button", onClick: () => handleSelectClue(c.number, 'down'), className: `flex items-start gap-2.5 p-2.5 rounded-lg border text-left transition ${isActive
|
|
244
|
+
? 'border-purple-600 bg-purple-100/90 shadow-sm ring-1 ring-purple-500'
|
|
245
|
+
: 'border-gray-200 bg-white hover:border-purple-300 hover:bg-purple-50/60'}`, children: [_jsx("span", { className: `flex items-center justify-center h-6 w-6 rounded-md text-xs font-bold ${isActive ? 'bg-purple-600 text-white' : 'bg-purple-100 text-purple-800'}`, children: c.number }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-semibold text-gray-800", children: c.clue }), isReviewMode && expectedWord && (_jsxs("div", { className: "text-xs mt-1 flex items-center gap-2", children: [_jsx("span", { className: "text-gray-500", children: "\u0110\u00E1p \u00E1n:" }), _jsx("span", { className: "font-bold text-green-700", children: expectedWord }), userWord && userWord !== expectedWord && (_jsx("span", { className: "line-through text-red-500 font-semibold", children: userWord }))] }))] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("span", { className: "text-xs text-gray-400 font-medium", children: ["(", c.length, ")"] }), isFilled && !isReviewMode && (_jsx("span", { className: "h-2 w-2 rounded-full bg-purple-500" }))] })] }, `down-${c.number}`));
|
|
246
|
+
}) })] })] }), _jsxs("div", { className: "lg:col-span-7 flex flex-col items-center gap-4 bg-gray-50/80 p-4 sm:p-6 rounded-2xl border-2 border-gray-200 shadow-sm", children: [!isReviewMode && (_jsxs("div", { className: "flex items-center gap-3 text-xs font-semibold text-gray-600 bg-white px-3 py-1.5 rounded-full border border-gray-200 shadow-sm", children: [_jsx("span", { children: "\u0110ang ch\u1ECDn:" }), _jsx("span", { className: "flex items-center gap-1 text-blue-700", children: activeDirection === 'across' ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "font-bold", children: ["Across ", activeClueNumber] }), _jsx("span", { className: "text-gray-400 font-normal", children: "(Ngang)" })] })) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "font-bold", children: ["Down ", activeClueNumber] }), _jsx("span", { className: "text-gray-400 font-normal", children: "(D\u1ECDc)" })] })) }), _jsx("span", { className: "text-gray-300", children: "|" }), _jsx("span", { className: "text-gray-500 font-normal", children: "Click \u0111\u00FAp \u00F4 giao nhau \u0111\u1EC3 \u0111\u1ED5i chi\u1EC1u" })] })), _jsx("div", { className: "overflow-x-auto max-w-full p-2", children: _jsx("div", { className: "inline-grid gap-1 sm:gap-1.5", style: {
|
|
247
|
+
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
248
|
+
}, children: Array.from({ length: rows }).map((_, rIdx) => {
|
|
249
|
+
const r = rIdx + 1;
|
|
250
|
+
return Array.from({ length: cols }).map((_, cIdx) => {
|
|
251
|
+
const c = cIdx + 1;
|
|
252
|
+
const key = `${r}-${c}`;
|
|
253
|
+
const cellInfo = cellMap.get(key);
|
|
254
|
+
const clueNum = clueNumberMap.get(key);
|
|
255
|
+
if (!cellInfo) {
|
|
256
|
+
return (_jsx("div", { className: "h-8 w-8 sm:h-10 sm:w-10 md:h-11 md:w-11 bg-transparent" }, key));
|
|
257
|
+
}
|
|
258
|
+
const isSelected = selectedCell?.r === r && selectedCell?.c === c;
|
|
259
|
+
const isWordActive = activeWordCells.some((cell) => cell.r === r && cell.c === c);
|
|
260
|
+
const reviewStatus = getCellReviewStatus(r, c);
|
|
261
|
+
const cellValue = answers[key] || '';
|
|
262
|
+
let cellStyle = 'border-gray-400 bg-white text-gray-800';
|
|
263
|
+
if (isReviewMode && reviewStatus) {
|
|
264
|
+
if (reviewStatus.isCorrect) {
|
|
265
|
+
cellStyle = 'border-green-500 bg-green-100 text-green-700 font-bold';
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
cellStyle = 'border-red-500 bg-red-100 text-red-700 font-bold';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else if (isSelected) {
|
|
272
|
+
cellStyle = 'border-blue-600 bg-blue-100 ring-2 ring-blue-500 text-blue-900 font-bold';
|
|
273
|
+
}
|
|
274
|
+
else if (isWordActive) {
|
|
275
|
+
cellStyle = 'border-blue-400 bg-blue-50 text-blue-900';
|
|
276
|
+
}
|
|
277
|
+
return (_jsxs("div", { onClick: () => handleCellClick(r, c), className: `relative flex items-center justify-center rounded-lg border-2 font-bold shadow-sm h-8 w-8 sm:h-10 sm:w-10 md:h-11 md:w-11 text-base sm:text-lg transition-all select-none cursor-pointer ${cellStyle}`, children: [clueNum && (_jsx("span", { className: "absolute top-0.5 left-1 text-[9px] sm:text-[10px] font-bold text-gray-500 leading-none", children: clueNum })), !isReviewMode ? (_jsx("input", { ref: (el) => {
|
|
278
|
+
if (el)
|
|
279
|
+
inputRefs.current.set(key, el);
|
|
280
|
+
else
|
|
281
|
+
inputRefs.current.delete(key);
|
|
282
|
+
}, type: "text", maxLength: 1, value: cellValue, onChange: (e) => handleInputChange(e.target.value, r, c), onKeyDown: (e) => handleKeyDown(e, r, c), onFocus: () => handleCellClick(r, c), className: "w-full h-full text-center bg-transparent uppercase font-bold outline-none cursor-pointer" })) : (_jsxs("div", { className: "flex flex-col items-center justify-center", children: [_jsx("span", { children: cellValue || '—' }), reviewStatus && !reviewStatus.isCorrect && (_jsx("span", { className: "absolute -bottom-1 -right-1 text-[9px] bg-green-600 text-white font-bold px-1 rounded-full shadow", children: reviewStatus.expected }))] })), isReviewMode && reviewStatus && (_jsx("span", { className: `absolute -top-1 -right-1 rounded-full p-0.5 text-white ${reviewStatus.isCorrect ? 'bg-green-500' : 'bg-red-500'}`, children: reviewStatus.isCorrect ? (_jsx(Check, { className: "h-2.5 w-2.5" })) : (_jsx(X, { className: "h-2.5 w-2.5" })) }))] }, key));
|
|
283
|
+
});
|
|
284
|
+
}) }) })] })] })] }));
|
|
285
|
+
}
|
|
@@ -18,4 +18,5 @@ export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRender
|
|
|
18
18
|
export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
|
|
19
19
|
export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
|
|
20
20
|
export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
|
|
21
|
+
export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
|
|
21
22
|
export { ArticlesViewer } from './ArticlesViewer';
|
|
@@ -22,4 +22,5 @@ export { MoversWordFillParagraphRenderer } from './MoversWordFillParagraphRender
|
|
|
22
22
|
export { MoversMatchByWritingAnswerRenderer } from './MoversMatchByWritingAnswerRenderer';
|
|
23
23
|
export { MoversWordFillStructuredFormRenderer } from './MoversWordFillStructuredFormRenderer';
|
|
24
24
|
export { MoversWordOrderAndMatchGroupRenderer } from './MoversWordOrderAndMatchGroupRenderer';
|
|
25
|
+
export { MoversCrosswordPuzzleRenderer } from './MoversCrosswordPuzzleRenderer';
|
|
25
26
|
export { ArticlesViewer } from './ArticlesViewer';
|
|
@@ -287,3 +287,33 @@ export interface MoversGridFillRendererProps {
|
|
|
287
287
|
caseSensitive?: boolean;
|
|
288
288
|
disabledCells?: string[];
|
|
289
289
|
}
|
|
290
|
+
export interface CrosswordClueItem {
|
|
291
|
+
number: number;
|
|
292
|
+
clue: string;
|
|
293
|
+
startRow: number;
|
|
294
|
+
startCol: number;
|
|
295
|
+
length: number;
|
|
296
|
+
isExample?: boolean;
|
|
297
|
+
}
|
|
298
|
+
export interface MoversCrosswordPuzzleRendererProps {
|
|
299
|
+
partNumber: number;
|
|
300
|
+
partName?: string;
|
|
301
|
+
questionCount: number;
|
|
302
|
+
instruction: string;
|
|
303
|
+
title?: string;
|
|
304
|
+
gridSize: {
|
|
305
|
+
rows: number;
|
|
306
|
+
cols: number;
|
|
307
|
+
};
|
|
308
|
+
markedColumn?: number;
|
|
309
|
+
hiddenWord?: string;
|
|
310
|
+
clues: {
|
|
311
|
+
across: CrosswordClueItem[];
|
|
312
|
+
down: CrosswordClueItem[];
|
|
313
|
+
};
|
|
314
|
+
answers: Record<string, string>;
|
|
315
|
+
correctAnswers?: Record<string, string>;
|
|
316
|
+
onAnswerChange: (key: string, value: string) => void;
|
|
317
|
+
isReviewMode?: boolean;
|
|
318
|
+
caseSensitive?: boolean;
|
|
319
|
+
}
|