@tinyweb_dev/oe-exam-sdk 0.1.1 → 0.1.3
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/api/exam-create.js +1 -179
- package/dist/api/exam-create.types.js +1 -1
- package/dist/api/exam-questions.d.ts +1 -1
- package/dist/api/exam-questions.js +1 -98
- package/dist/api/exam-questions.types.d.ts +45 -1
- package/dist/api/exam-questions.types.js +1 -1
- package/dist/api/exam-taking.js +1 -73
- package/dist/api/exam-taking.types.js +1 -1
- package/dist/api/index.js +1 -6
- package/dist/api/result-review.js +1 -50
- package/dist/api/result-review.types.js +1 -1
- package/dist/components/CueCardEditor.js +1 -58
- package/dist/components/common/ResolvedImage.js +1 -15
- package/dist/components/exams/CompletionDialog.d.ts +8 -0
- package/dist/components/exams/CompletionDialog.js +1 -0
- package/dist/components/exams/CreateExamInfoForm.d.ts +4 -1
- package/dist/components/exams/CreateExamInfoForm.js +1 -35
- package/dist/components/exams/CreateExamPageContainer.d.ts +3 -0
- package/dist/components/exams/CreateExamPageContainer.js +1 -103
- package/dist/components/exams/ExamCreator.d.ts +9 -3
- package/dist/components/exams/ExamCreator.js +1 -89
- package/dist/components/exams/ExamPartTabs.d.ts +1 -1
- package/dist/components/exams/ExamPartTabs.js +1 -40
- package/dist/components/exams/ExamPreviewDialog.d.ts +20 -0
- package/dist/components/exams/ExamPreviewDialog.js +1 -0
- package/dist/components/exams/ExamQuestionGrid.d.ts +1 -1
- package/dist/components/exams/ExamQuestionGrid.js +1 -18
- package/dist/components/exams/ExamQuestionsPageContainer.d.ts +3 -0
- package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
- package/dist/components/exams/ExamViewDialog.d.ts +24 -0
- package/dist/components/exams/ExamViewDialog.js +1 -0
- package/dist/components/exams/PartAudioUploader.js +1 -9
- package/dist/components/exams/TemplateSelector.js +1 -66
- package/dist/components/exams/UnsavedChangesDialog.d.ts +8 -0
- package/dist/components/exams/UnsavedChangesDialog.js +1 -0
- package/dist/components/exams/api.d.ts +1 -1
- package/dist/components/exams/api.js +1 -3
- package/dist/components/exams/create-exam.texts.d.ts +58 -0
- package/dist/components/exams/create-exam.texts.js +1 -0
- package/dist/components/exams/create-exam.validation.js +1 -18
- package/dist/components/exams/exam-question-status.d.ts +9 -0
- package/dist/components/exams/exam-question-status.js +1 -0
- package/dist/components/exams/exam-template.utils.js +1 -18
- package/dist/components/exams/index.d.ts +11 -1
- package/dist/components/exams/index.js +1 -10
- package/dist/components/exams/take/ExamTakingPageContainer.js +1 -153
- package/dist/components/exams/take/GenericQuestionRenderer.js +1 -14
- package/dist/components/exams/take/exam-taking.utils.js +1 -88
- package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -24
- package/dist/components/exams/take/index.js +1 -2
- package/dist/components/exams/take/types.js +1 -1
- package/dist/components/index.js +1 -42
- package/dist/components/questions/_shared/config/question-types.config.js +1 -333
- package/dist/components/questions/_shared/constants/color-palette.js +1 -21
- package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +1 -105
- package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +1 -222
- package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +1 -140
- package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +1 -123
- package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +1 -50
- package/dist/components/questions/_shared/hooks/useQuestionFormState.js +1 -54
- package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +1 -123
- package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +1 -121
- package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +1 -70
- package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +1 -194
- package/dist/components/questions/_shared/index.js +1 -7
- package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
- package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
- package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +1 -3
- package/dist/components/questions/_shared/types/color-region.config.js +1 -1
- package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
- package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
- package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
- package/dist/components/questions/_shared/types/index.js +1 -12
- package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +1 -3
- package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
- package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +1 -3
- package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
- package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
- package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
- package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
- package/dist/components/questions/_shared/types/question-group.type.js +1 -11
- package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
- package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
- package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
- package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
- package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
- package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
- package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
- package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
- package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +1 -3
- package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
- package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
- package/dist/components/questions/_shared/utils/answer-builder.js +1 -79
- package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +1 -39
- package/dist/components/questions/_shared/utils/id-generator.js +1 -3
- package/dist/components/questions/_shared/utils/question-validation.js +1 -21
- package/dist/components/questions/components/ColorNode.js +1 -85
- package/dist/components/questions/components/ColorRegionClient.js +1 -545
- package/dist/components/questions/components/ColorRegionCreator.js +1 -420
- package/dist/components/questions/components/ColorRegionNode.js +1 -59
- package/dist/components/questions/components/CreatorGuide.js +1 -103
- package/dist/components/questions/components/NameNode.js +1 -26
- package/dist/components/questions/components/QuestionBankOption.js +1 -46
- package/dist/components/questions/components/QuestionBasicInfoForm.js +1 -9
- package/dist/components/questions/components/QuestionCommonFields.js +1 -33
- package/dist/components/questions/components/QuestionSearchDropdown.js +1 -258
- package/dist/components/questions/components/QuestionTypeSelector.js +1 -32
- package/dist/components/questions/components/QuestionTypeSpecificForms.js +1 -6
- package/dist/components/questions/components/RegionNode.js +1 -94
- package/dist/components/questions/components/index.js +1 -14
- package/dist/components/questions/creator/QuestionCreator.js +1 -90
- package/dist/components/questions/creator/QuestionGroupCreator.js +1 -119
- package/dist/components/questions/creator/dedicated-component-router.js +1 -79
- package/dist/components/questions/creator/index.js +1 -3
- package/dist/components/questions/creator/question-type-registry.js +1 -80
- package/dist/components/questions/groups/ArticlesGroupCard.js +1 -121
- package/dist/components/questions/groups/BasicQuestionGroupCard.js +1 -50
- package/dist/components/questions/groups/DocumentGroupCard.js +1 -114
- package/dist/components/questions/groups/MatchAnswerGroupCard.js +1 -25
- package/dist/components/questions/index.js +1 -10
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +1 -37
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +1 -351
- package/dist/components/questions/types/answer-the-question/register.js +1 -67
- package/dist/components/questions/types/answer-the-question/transform.js +1 -36
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +1 -141
- package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +1 -118
- package/dist/components/questions/types/arrange-letters-into-words/index.js +1 -2
- package/dist/components/questions/types/arrange-letters-into-words/register.js +1 -4
- package/dist/components/questions/types/arrange-letters-into-words/transform.js +1 -24
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +1 -64
- package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +1 -169
- package/dist/components/questions/types/choose-correct-adjective/index.js +1 -2
- package/dist/components/questions/types/choose-correct-adjective/register.js +1 -55
- package/dist/components/questions/types/choose-correct-adjective/transform.js +1 -16
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -131
- package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -581
- package/dist/components/questions/types/choose-the-correct-answer/index.js +1 -2
- package/dist/components/questions/types/choose-the-correct-answer/register.js +1 -101
- package/dist/components/questions/types/choose-the-correct-answer/transform.js +1 -43
- package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -275
- package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +1 -448
- package/dist/components/questions/types/fill-in-blank/index.js +1 -2
- package/dist/components/questions/types/fill-in-blank/register.js +1 -138
- package/dist/components/questions/types/fill-in-blank/transform.js +1 -59
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +1 -117
- package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +1 -671
- package/dist/components/questions/types/fill-missing-words-in-grid/index.js +1 -2
- package/dist/components/questions/types/fill-missing-words-in-grid/register.js +1 -37
- package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +1 -15
- package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +1 -262
- package/dist/components/questions/types/gn-speaking-interview/register.js +1 -121
- package/dist/components/questions/types/gn-speaking-interview/transform.js +1 -48
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +1 -16
- package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +1 -27
- package/dist/components/questions/types/image-object-matching/index.js +1 -2
- package/dist/components/questions/types/image-object-matching/register.js +1 -3
- package/dist/components/questions/types/image-object-matching/transform.js +1 -2
- package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +1 -102
- package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +1 -344
- package/dist/components/questions/types/label-the-picture/index.js +1 -2
- package/dist/components/questions/types/label-the-picture/register.js +1 -54
- package/dist/components/questions/types/label-the-picture/transform.js +1 -28
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +1 -78
- package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +1 -232
- package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +1 -2
- package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +1 -82
- package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +1 -27
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +1 -70
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +1 -386
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +1 -2
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +1 -100
- package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +1 -33
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +1 -106
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +1 -457
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +1 -2
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +1 -110
- package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +1 -35
- package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +1 -8
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +1 -22
- package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +1 -291
- package/dist/components/questions/types/listen-and-speak-answer/index.js +1 -2
- package/dist/components/questions/types/listen-and-speak-answer/register.js +1 -84
- package/dist/components/questions/types/listen-and-speak-answer/transform.js +1 -27
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +1 -32
- package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +1 -295
- package/dist/components/questions/types/listen-and-speak-compare-images/register.js +1 -79
- package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +1 -27
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +1 -34
- package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +1 -285
- package/dist/components/questions/types/listen-and-speak-image-group/register.js +1 -81
- package/dist/components/questions/types/listen-and-speak-image-group/transform.js +1 -26
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +1 -38
- package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +1 -385
- package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +1 -87
- package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +1 -32
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +1 -247
- package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +1 -59
- package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +1 -18
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +1 -256
- package/dist/components/questions/types/listen-and-speak-question-list/register.js +1 -81
- package/dist/components/questions/types/listen-and-speak-question-list/transform.js +1 -26
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +1 -26
- package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +1 -230
- package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +1 -56
- package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +1 -16
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +1 -72
- package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +1 -230
- package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
- package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +1 -66
- package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +1 -42
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +1 -96
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +1 -238
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +1 -2
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +1 -76
- package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +1 -19
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +1 -134
- package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +1 -617
- package/dist/components/questions/types/look-picture-fill-word-hint/index.js +1 -2
- package/dist/components/questions/types/look-picture-fill-word-hint/register.js +1 -82
- package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +1 -29
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +1 -38
- package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +1 -359
- package/dist/components/questions/types/match-by-writing-answer/index.js +1 -3
- package/dist/components/questions/types/match-by-writing-answer/register.js +1 -58
- package/dist/components/questions/types/match-by-writing-answer/transform.js +1 -61
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +1 -16
- package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +1 -23
- package/dist/components/questions/types/read-and-color-objects/index.js +1 -2
- package/dist/components/questions/types/read-and-color-objects/register.js +1 -3
- package/dist/components/questions/types/read-and-color-objects/transform.js +1 -36
- package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +1 -125
- package/dist/components/questions/types/read-displayed-content/register.js +1 -64
- package/dist/components/questions/types/read-displayed-content/transform.js +1 -12
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +1 -64
- package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +1 -511
- package/dist/components/questions/types/read-passage-and-answer-questions/index.js +1 -2
- package/dist/components/questions/types/read-passage-and-answer-questions/register.js +1 -58
- package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +1 -28
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +1 -373
- package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -41
- package/dist/components/questions/types/speaking-conversation/register.js +1 -110
- package/dist/components/questions/types/speaking-conversation/transform.js +1 -34
- package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -74
- package/dist/components/questions/types/speaking-cue-card/register.js +1 -52
- package/dist/components/questions/types/speaking-cue-card/transform.js +1 -12
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +1 -10
- package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +1 -161
- package/dist/components/questions/types/speaking-describe-image/index.js +1 -2
- package/dist/components/questions/types/speaking-describe-image/register.js +1 -69
- package/dist/components/questions/types/speaking-describe-image/transform.js +1 -16
- package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +1 -234
- package/dist/components/questions/types/spontaneous-qa/register.js +1 -93
- package/dist/components/questions/types/spontaneous-qa/transform.js +1 -34
- package/dist/components/questions/types/true-false/TrueFalseClient.js +1 -75
- package/dist/components/questions/types/true-false/TrueFalseCreator.js +1 -244
- package/dist/components/questions/types/true-false/index.js +1 -2
- package/dist/components/questions/types/true-false/register.js +1 -77
- package/dist/components/questions/types/true-false/transform.js +1 -32
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +1 -233
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +1 -631
- package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +1 -28
- package/dist/components/questions/types/word-fill-paragraph/index.js +1 -2
- package/dist/components/questions/types/word-fill-paragraph/register.js +1 -136
- package/dist/components/questions/types/word-fill-paragraph/transform.js +1 -56
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +1 -138
- package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +1 -261
- package/dist/components/questions/types/word-fill-structured-form/index.js +1 -2
- package/dist/components/questions/types/word-fill-structured-form/register.js +1 -67
- package/dist/components/questions/types/word-fill-structured-form/transform.js +1 -40
- package/dist/components/questions/types/word-ordering/WordOrderingClient.js +1 -84
- package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +1 -192
- package/dist/components/questions/types/word-ordering/index.js +1 -2
- package/dist/components/questions/types/word-ordering/register.js +1 -46
- package/dist/components/questions/types/word-ordering/transform.js +1 -24
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +1 -98
- package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +1 -320
- package/dist/components/questions/types/write-a-short-letter/index.js +1 -2
- package/dist/components/questions/types/write-a-short-letter/register.js +1 -68
- package/dist/components/questions/types/write-a-short-letter/transform.js +1 -25
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +1 -61
- package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +1 -196
- package/dist/components/questions/types/write-correct-verb-form/index.js +1 -2
- package/dist/components/questions/types/write-correct-verb-form/register.js +1 -62
- package/dist/components/questions/types/write-correct-verb-form/transform.js +1 -17
- package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +1 -22
- package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +1 -174
- package/dist/components/questions/types/write-sentences/index.js +1 -2
- package/dist/components/questions/types/write-sentences/register.js +1 -58
- package/dist/components/questions/types/write-sentences/transform.js +1 -28
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +1 -96
- package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +1 -161
- package/dist/components/questions/types/write-short-paragraph/index.js +1 -2
- package/dist/components/questions/types/write-short-paragraph/register.js +1 -4
- package/dist/components/questions/types/write-short-paragraph/transform.js +1 -21
- package/dist/components/questions/viewer/QuestionGroupViewer.js +1 -36
- package/dist/components/questions/viewer/QuestionViewer.js +1 -446
- package/dist/components/questions/viewer/index.js +1 -2
- package/dist/components/results/ResultReviewPageContainer.js +1 -98
- package/dist/components/results/ReviewQuestionRenderer.js +1 -72
- package/dist/components/results/index.js +1 -2
- package/dist/components/results/result-review.utils.js +1 -6
- package/dist/components/results/review-data.js +1 -150
- package/dist/components/results/review-renderer.utils.js +1 -97
- package/dist/components/results/review-types.js +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +1 -138
- package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +1 -12
- package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +1 -18
- package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +1 -11
- package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +1 -11
- package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +1 -9
- package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +1 -13
- package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +1 -8
- package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +1 -6
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +1 -5
- package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +1 -9
- package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +1 -41
- package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -25
- package/dist/components/themes/cambridge-yle/index.js +1 -13
- package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +1 -24
- package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +1 -88
- package/dist/components/themes/english-certification/EcWordFillParagraph.js +1 -55
- package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +1 -156
- package/dist/components/themes/english-certification/EcWritingTask.js +1 -34
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -114
- package/dist/components/themes/english-certification/EnglishCertificationBrand.js +1 -11
- package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +1 -21
- package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +1 -15
- package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +1 -19
- package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +1 -148
- package/dist/components/themes/english-certification/EnglishCertificationHeader.js +1 -31
- package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +1 -71
- package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +1 -33
- package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +1 -16
- package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +1 -22
- package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +1 -37
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -72
- package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +1 -90
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +1 -18
- package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +1 -29
- package/dist/components/themes/english-certification/english-certification-theme.js +1 -58
- package/dist/components/themes/english-certification/grouped-question-utils.js +1 -218
- package/dist/components/themes/english-certification/index.js +1 -21
- package/dist/components/themes/index.js +1 -6
- package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +1 -170
- package/dist/components/themes/summer-sky/SummerSkyLayout.js +1 -221
- package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +1 -18
- package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +1 -194
- package/dist/components/themes/types.js +1 -1
- package/dist/components/ui/alert-dialog.js +1 -26
- package/dist/components/ui/alert.js +1 -22
- package/dist/components/ui/avatar.js +1 -12
- package/dist/components/ui/badge.js +1 -38
- package/dist/components/ui/button.js +1 -35
- package/dist/components/ui/calendar.js +1 -76
- package/dist/components/ui/card.js +1 -16
- package/dist/components/ui/carousel.js +1 -91
- package/dist/components/ui/checkbox.js +1 -9
- package/dist/components/ui/collapsible.js +1 -13
- package/dist/components/ui/command.js +1 -34
- package/dist/components/ui/confirm-dialog.js +1 -49
- package/dist/components/ui/dialog.js +1 -23
- package/dist/components/ui/drawer.js +1 -35
- package/dist/components/ui/dropdown-menu.js +1 -33
- package/dist/components/ui/file-upload.js +1 -155
- package/dist/components/ui/form.js +1 -62
- package/dist/components/ui/hover-card.js +1 -10
- package/dist/components/ui/image-preview.js +1 -29
- package/dist/components/ui/input.js +1 -8
- package/dist/components/ui/label.js +1 -9
- package/dist/components/ui/language-switcher.js +1 -15
- package/dist/components/ui/multi-select.js +1 -90
- package/dist/components/ui/points-input.js +1 -33
- package/dist/components/ui/popover.js +1 -26
- package/dist/components/ui/progress.js +1 -16
- package/dist/components/ui/radio-group.js +1 -12
- package/dist/components/ui/scroll-area.js +1 -11
- package/dist/components/ui/select.js +1 -38
- package/dist/components/ui/separator.js +1 -8
- package/dist/components/ui/sheet.js +1 -40
- package/dist/components/ui/single-select.js +1 -76
- package/dist/components/ui/skeleton.js +1 -6
- package/dist/components/ui/switch.js +1 -8
- package/dist/components/ui/table.js +1 -20
- package/dist/components/ui/tabs.js +1 -17
- package/dist/components/ui/textarea.js +1 -8
- package/dist/components/ui/tooltip.js +1 -11
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -19
- package/dist/mcp/config.js +1 -32
- package/dist/mcp/http-client.js +1 -48
- package/dist/mcp/index.js +1 -29
- package/dist/mcp/tools/_helpers.js +1 -25
- package/dist/mcp/tools/exams.js +1 -439
- package/dist/mcp/tools/index.js +1 -6
- package/dist/mcp/tools/results.js +1 -19
- package/dist/shared/constants/ApiConstant.js +1 -276
- package/dist/shared/constants/QueryKeyConstant.js +1 -112
- package/dist/shared/constants/exam-level.enum.js +1 -41
- package/dist/shared/constants/landings/placement-test.constants.js +1 -431
- package/dist/shared/constants/question-skills.js +1 -267
- package/dist/shared/constants/routes.js +1 -81
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.js +1 -7
- package/dist/shared/lib/hooks/index.js +1 -10
- package/dist/shared/lib/hooks/useAlertDialog.js +1 -57
- package/dist/shared/lib/hooks/useAudioPlayCounter.js +1 -30
- package/dist/shared/lib/hooks/useAudioPreview.js +1 -81
- package/dist/shared/lib/hooks/useAudioUpload.js +1 -24
- package/dist/shared/lib/hooks/useConfirmDialog.js +1 -89
- package/dist/shared/lib/hooks/useDebouncedCallback.js +1 -32
- package/dist/shared/lib/hooks/useDelayedAutoPlay.js +1 -12
- package/dist/shared/lib/hooks/useFileUpload.js +1 -24
- package/dist/shared/lib/hooks/useImageUrl.js +1 -5
- package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -10
- package/dist/shared/lib/hooks/useReactHookForm.js +1 -31
- package/dist/shared/lib/hooks/useToast.js +1 -62
- package/dist/shared/lib/i18n/index.js +1 -54
- package/dist/shared/lib/i18n/messages/en/admin.js +1 -2104
- package/dist/shared/lib/i18n/messages/en/auth.js +1 -86
- package/dist/shared/lib/i18n/messages/en/common.js +1 -140
- package/dist/shared/lib/i18n/messages/en/file-upload.js +1 -18
- package/dist/shared/lib/i18n/messages/en/student.js +1 -133
- package/dist/shared/lib/i18n/messages/en/terms.js +1 -37
- package/dist/shared/lib/i18n/messages/en.js +1 -15
- package/dist/shared/lib/i18n/messages/vi/admin.js +1 -2104
- package/dist/shared/lib/i18n/messages/vi/auth.js +1 -86
- package/dist/shared/lib/i18n/messages/vi/common.js +1 -140
- package/dist/shared/lib/i18n/messages/vi/file-upload.js +1 -18
- package/dist/shared/lib/i18n/messages/vi/student.js +1 -133
- package/dist/shared/lib/i18n/messages/vi/terms.js +1 -37
- package/dist/shared/lib/i18n/messages/vi.js +1 -15
- package/dist/shared/lib/i18n/types.js +1 -1
- package/dist/shared/lib/stores/examQuestionStore.js +1 -212
- package/dist/shared/lib/stores/localeStore.js +1 -10
- package/dist/shared/lib/utils/public-page.js +1 -44
- package/dist/shared/lib/utils/question-reverse-transform.d.ts +6 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +1 -0
- package/dist/shared/lib/utils/question-transform-types.js +1 -1
- package/dist/shared/lib/utils/question-transform-validation.d.ts +6 -0
- package/dist/shared/lib/utils/question-transform-validation.js +1 -0
- package/dist/shared/lib/utils/question-transform.d.ts +6 -2
- package/dist/shared/lib/utils/question-transform.js +1 -174
- package/dist/shared/lib/utils.js +1 -168
- package/dist/shared/lib/validation/choose-correct-answer.validation.js +1 -111
- package/dist/shared/lib/validation/label-the-picture.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +1 -46
- package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +1 -21
- package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +1 -34
- package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +1 -40
- package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +1 -45
- package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +1 -39
- package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +1 -32
- package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +1 -19
- package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +1 -28
- package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +1 -12
- package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +1 -47
- package/dist/shared/lib/validation/read-and-color-objects.validation.js +1 -29
- package/dist/shared/lib/validation/read-displayed-content.validation.js +1 -28
- package/dist/shared/lib/validation/speaking-conversation.validation.js +1 -75
- package/dist/shared/lib/validation/speaking-describe-image.validation.js +1 -31
- package/dist/shared/lib/validation/spontaneous-qa.validation.js +1 -48
- package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +1 -38
- package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +1 -39
- package/dist/shared/lib/validation/word-fill-paragraph.validation.js +1 -43
- package/dist/shared/lib/validation/word-ordering.validation.js +1 -33
- package/dist/shared/types/branch.types.js +1 -6
- package/dist/shared/types/common.types.js +1 -50
- package/dist/shared/types/entities/exam-schedule.types.js +1 -5
- package/dist/shared/types/entities/exam.types.js +1 -1
- package/dist/shared/types/entities/question.types.js +1 -4
- package/dist/shared/types/entities/result.types.js +1 -4
- package/dist/shared/types/entities/user.types.js +1 -4
- package/dist/shared/types/exam-taking.types.js +1 -2
- package/dist/shared/types/history.types.js +1 -34
- package/dist/shared/types/index.js +1 -1
- package/dist/shared/types/questions/choose-correct-adjective.js +1 -4
- package/dist/shared/types/questions/choose-the-correct-answer.js +1 -9
- package/dist/shared/types/questions/fill-missing-words-in-grid.js +1 -11
- package/dist/shared/types/questions/index.js +1 -40
- package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-info-exchange.js +1 -3
- package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +1 -3
- package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
- package/dist/shared/types/questions/listen-and-speak-with-story-images.js +1 -3
- package/dist/shared/types/questions/look-picture-choose-correct-answer.js +1 -5
- package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +1 -9
- package/dist/shared/types/questions/look-picture-fill-word-hint.js +1 -7
- package/dist/shared/types/questions/read-displayed-content.js +1 -1
- package/dist/shared/types/questions/speaking-conversation.js +1 -7
- package/dist/shared/types/questions/speaking-describe-image.js +1 -1
- package/dist/shared/types/questions/spontaneous-qa.js +1 -1
- package/dist/shared/types/questions/write-correct-verb-form.js +1 -12
- package/dist/shared/types/role.types.js +1 -1
- package/dist/shared/types/structure-management.js +1 -28
- package/dist/shared/types/student.types.js +1 -54
- package/dist/shared/types/teacher-dashboard-rooms.js +1 -2
- package/package.json +12 -3
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { type UseFormReturn } from 'react-hook-form';
|
|
2
2
|
import type { ExamFormData, ExamLevelOption } from '../../api/exam-create';
|
|
3
|
+
import type { CreateExamTexts } from './create-exam.texts';
|
|
3
4
|
export interface CreateExamInfoFormProps {
|
|
4
5
|
form: UseFormReturn<ExamFormData>;
|
|
5
6
|
examLevels: ExamLevelOption[];
|
|
6
7
|
isLoadingExamLevels?: boolean;
|
|
8
|
+
/** Optional label overrides (e.g. to reuse this form for homework). */
|
|
9
|
+
texts?: CreateExamTexts;
|
|
7
10
|
}
|
|
8
|
-
export declare function CreateExamInfoForm({ form, examLevels, isLoadingExamLevels }: CreateExamInfoFormProps): import("react").JSX.Element;
|
|
11
|
+
export declare function CreateExamInfoForm({ form, examLevels, isLoadingExamLevels, texts, }: CreateExamInfoFormProps): import("react").JSX.Element;
|
|
@@ -1,35 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
-
import { Controller } from 'react-hook-form';
|
|
5
|
-
import { FileText, Loader2 } from 'lucide-react';
|
|
6
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
|
|
7
|
-
import { Input } from '../../components/ui/input';
|
|
8
|
-
import { Label } from '../../components/ui/label';
|
|
9
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
|
|
10
|
-
import { Textarea } from '../../components/ui/textarea';
|
|
11
|
-
import { useT } from '../../shared/lib/i18n';
|
|
12
|
-
export function CreateExamInfoForm({ form, examLevels, isLoadingExamLevels = false }) {
|
|
13
|
-
const t = useT();
|
|
14
|
-
const isCodeManuallyEdited = useRef(false);
|
|
15
|
-
const nameValue = form.watch('name');
|
|
16
|
-
const setValueRef = useRef(form.setValue);
|
|
17
|
-
setValueRef.current = form.setValue;
|
|
18
|
-
const generateCodeFromName = useCallback((name) => name.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/đ/g, 'd').replace(/Đ/g, 'D').toLowerCase().trim().replace(/[^a-z0-9\s-]/g, '').replace(/[\s]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 100), []);
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (isCodeManuallyEdited.current)
|
|
21
|
-
return;
|
|
22
|
-
setValueRef.current('code', nameValue ? generateCodeFromName(nameValue) : '', { shouldValidate: false });
|
|
23
|
-
}, [nameValue, generateCodeFromName]);
|
|
24
|
-
const hasError = (field) => !!form.formState.errors[field];
|
|
25
|
-
const error = (field) => form.formState.errors[field]?.message;
|
|
26
|
-
return (_jsxs(Card, { className: "border border-gray-200", children: [_jsxs(CardHeader, { className: "border-b border-gray-100 bg-gray-50/50", children: [_jsxs(CardTitle, { className: "flex items-center gap-2 text-base font-semibold", children: [_jsx(FileText, { className: "h-4 w-4 text-blue-600" }), t('admin.exams.form.basicInfo')] }), _jsx(CardDescription, { children: t('admin.exams.form.requiredHint') })] }), _jsxs(CardContent, { className: "space-y-4 pt-6", children: [_jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsx(Field, { label: t('admin.exams.form.nameLabel'), error: error('name'), required: true, children: _jsx(Input, { id: "name", maxLength: 255, className: hasError('name') ? 'border-red-500' : '', placeholder: t('admin.exams.form.namePlaceholder'), ...form.register('name') }) }), _jsx(Field, { label: t('admin.exams.form.codeLabel'), error: error('code'), required: true, children: _jsx(CodeInput, { form: form, hasError: hasError('code'), onManualEdit: (value) => { isCodeManuallyEdited.current = value.length > 0; } }) }), _jsx(Field, { label: t('admin.exams.form.totalPoints'), error: error('totalPoints'), required: true, children: _jsx(Controller, { name: "totalPoints", control: form.control, render: ({ field }) => _jsx(Input, { id: "totalPoints", inputMode: "numeric", value: field.value, onBlur: field.onBlur, onChange: (event) => field.onChange(event.target.value.replace(/[^0-9]/g, '')), className: hasError('totalPoints') ? 'border-red-500' : '' }) }) }), _jsx(Field, { label: t('admin.exams.form.level'), error: error('level'), children: _jsx(Controller, { name: "level", control: form.control, render: ({ field }) => _jsxs(Select, { value: field.value, onValueChange: field.onChange, disabled: isLoadingExamLevels, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: isLoadingExamLevels ? t('admin.exams.form.loading') : t('admin.exams.form.levelPlaceholder') }) }), _jsx(SelectContent, { children: isLoadingExamLevels ? _jsx("div", { className: "flex justify-center p-4", children: _jsx(Loader2, { className: "h-4 w-4 animate-spin" }) }) : examLevels.map((level) => _jsx(SelectItem, { value: level.id, children: level.name }, level.id)) })] }) }) }), _jsx(Field, { label: t('admin.exams.form.difficulty'), error: error('difficulty'), children: _jsx(Controller, { name: "difficulty", control: form.control, render: ({ field }) => _jsxs(Select, { value: field.value, onValueChange: field.onChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: t('admin.exams.form.difficultyPlaceholder') }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "easy", children: t('admin.exams.difficulty.easy') }), _jsx(SelectItem, { value: "medium", children: t('admin.exams.difficulty.medium') }), _jsx(SelectItem, { value: "hard", children: t('admin.exams.difficulty.hard') })] })] }) }) }), _jsx(Field, { label: t('admin.exams.form.duration'), error: error('duration'), children: _jsx(Input, { id: "duration", inputMode: "numeric", placeholder: "60", className: hasError('duration') ? 'border-red-500' : '', ...form.register('duration') }) })] }), _jsx(Field, { label: t('admin.exams.form.description'), error: error('description'), children: _jsx(Textarea, { id: "description", rows: 4, maxLength: 2000, placeholder: t('admin.exams.form.descriptionPlaceholder'), ...form.register('description') }) })] })] }));
|
|
27
|
-
}
|
|
28
|
-
function CodeInput({ form, hasError, onManualEdit }) {
|
|
29
|
-
const t = useT();
|
|
30
|
-
const { onChange, ...register } = form.register('code');
|
|
31
|
-
return _jsx(Input, { id: "code", maxLength: 100, className: hasError ? 'border-red-500' : '', placeholder: t('admin.exams.form.codePlaceholder'), ...register, onChange: (event) => { onChange(event); onManualEdit(event.target.value); } });
|
|
32
|
-
}
|
|
33
|
-
function Field({ label, error, required, children }) {
|
|
34
|
-
return _jsxs("div", { children: [_jsxs(Label, { children: [label, " ", required && _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx("div", { className: "mt-1", children: children }), error && _jsx("p", { className: "mt-1 text-sm text-red-600", children: error })] });
|
|
35
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useCallback as a,useEffect as n,useRef as o}from"react";import{Controller as l}from"react-hook-form";import{FileText as i,Loader2 as t}from"lucide-react";import{Card as d,CardContent as c,CardDescription as m,CardHeader as s,CardTitle as u}from"../../components/ui/card";import{Input as h}from"../../components/ui/input";import{Label as f}from"../../components/ui/label";import{Select as p,SelectContent as g,SelectItem as x,SelectTrigger as b,SelectValue as N}from"../../components/ui/select";import{Textarea as v}from"../../components/ui/textarea";import{useT as y}from"../../shared/lib/i18n";const C=["EASY","MEDIUM","HARD"];export function CreateExamInfoForm({form:f,examLevels:S,isLoadingExamLevels:w=!1,texts:M}){const q=y(),D=o(!1),I=f.watch("name"),V=o(f.setValue);V.current=f.setValue;const j=a(e=>e.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/đ/g,"d").replace(/Đ/g,"D").toLowerCase().trim().replace(/[^a-z0-9\s-]/g,"").replace(/[\s]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").substring(0,100),[]);n(()=>{D.current||V.current("code",I?j(I):"",{shouldValidate:!1})},[I,j]);const A=e=>Boolean(f.formState.errors[e]),H=e=>f.formState.errors[e]?.message;return r(d,{className:"border border-gray-200",children:[r(s,{className:"border-b border-gray-100 bg-gray-50/50",children:[r(u,{className:"flex items-center gap-2 text-base font-semibold",children:[e(i,{className:"h-4 w-4 text-blue-600"}),M?.basicInfo??q("admin.exams.form.basicInfo")]}),e(m,{children:M?.requiredHint??q("admin.exams.form.requiredHint")})]}),e(c,{className:"space-y-4 pt-6",children:r("div",{className:"grid grid-cols-1 gap-4 md:grid-cols-2",children:[e(P,{label:M?.nameLabel??q("admin.exams.form.nameLabel"),error:H("name"),required:!0,hint:q("admin.exams.form.charCount",{count:String(I?.length||0),max:String(255)}),children:e(h,{id:"name",maxLength:255,className:A("name")?"border-red-500 focus:ring-red-500":"",placeholder:M?.namePlaceholder??q("admin.exams.form.namePlaceholder"),...f.register("name")})}),e(P,{label:M?.codeLabel??q("admin.exams.form.codeLabel"),error:H("code"),required:!0,hint:q("admin.exams.form.charCount",{count:String(f.watch("code")?.length||0),max:String(100)}),children:e(L,{form:f,hasError:A("code"),placeholder:M?.codePlaceholder??q("admin.exams.form.codePlaceholder"),onManualEdit:e=>{D.current=e.length>0}})}),e(P,{label:q("admin.exams.form.totalPoints"),error:H("totalPoints"),required:!0,children:e(l,{name:"totalPoints",control:f.control,render:({field:r})=>e(h,{id:"totalPoints",type:"text",inputMode:"numeric",placeholder:"100",value:r.value,onBlur:r.onBlur,onChange:e=>r.onChange(e.target.value.replace(/[^0-9]/g,"")),className:A("totalPoints")?"border-red-500 focus:ring-red-500":""})})}),e(P,{label:q("admin.exams.form.level"),error:H("level"),children:e(l,{name:"level",control:f.control,render:({field:a})=>r(p,{value:a.value,onValueChange:a.onChange,disabled:w,children:[e(b,{className:A("level")?"border-red-500 focus:ring-red-500":"",children:e(N,{placeholder:q(w?"admin.exams.form.loading":"admin.exams.form.levelPlaceholder")})}),e(g,{children:w?e("div",{className:"flex items-center justify-center p-4",children:e(t,{className:"h-4 w-4 animate-spin text-gray-400"})}):S.map(r=>e(x,{value:r.id,children:r.name},r.id))})]})})}),e(P,{label:q("admin.exams.form.difficulty"),error:H("difficulty"),children:e(l,{name:"difficulty",control:f.control,render:({field:a})=>r(p,{value:a.value,onValueChange:a.onChange,children:[e(b,{className:A("difficulty")?"border-red-500 focus:ring-red-500":"",children:e(N,{placeholder:q("admin.exams.form.difficultyPlaceholder")})}),e(g,{children:C.map(r=>e(x,{value:r,children:E(r,q)},r))})]})})}),e(P,{label:M?.durationLabel??q("admin.exams.form.duration"),error:H("duration"),children:e(h,{id:"duration",type:"number",inputMode:"numeric",min:5,max:300,placeholder:"45",className:A("duration")?"border-red-500 focus:ring-red-500":"",...f.register("duration")})}),e(P,{className:"md:col-span-2",label:M?.descriptionLabel??q("admin.exams.form.description"),error:H("description"),hint:q("admin.exams.form.charCount",{count:String(f.watch("description")?.length||0),max:String(2e3)}),children:e(v,{id:"description",rows:3,maxLength:2e3,placeholder:M?.descriptionPlaceholder??q("admin.exams.form.descriptionPlaceholder"),className:A("description")?"border-red-500 focus:ring-red-500":"",...f.register("description")})})]})})]})}function L({form:r,hasError:a,placeholder:n,onManualEdit:o}){const{onChange:l,...i}=r.register("code");return e(h,{id:"code",maxLength:100,className:a?"border-red-500 focus:ring-red-500":"",placeholder:n,...i,onChange:e=>{l(e),o(e.target.value)}})}function P({label:a,error:n,hint:o,required:l,className:i,children:t}){return r("div",{className:i,children:[r(f,{children:[a," ",l&&e("span",{className:"text-red-500",children:"*"})]}),e("div",{className:"mt-1",children:t}),n&&e("p",{className:"mt-1 text-sm text-red-600",children:n}),o&&e("p",{className:"mt-1 text-xs text-gray-500",children:o})]})}function E(e,r){switch(e){case"EASY":return r("admin.exams.form.easy");case"MEDIUM":return r("admin.exams.form.medium");case"HARD":return r("admin.exams.form.hard");default:return e}}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { UserRole } from '../../shared/types/common.types';
|
|
2
2
|
import type { CreateExamSdkApi } from '../../api/exam-create';
|
|
3
|
+
import type { CreateExamTexts } from './create-exam.texts';
|
|
3
4
|
export interface CreateExamPageContainerProps {
|
|
4
5
|
role?: UserRole.ADMIN | UserRole.TEACHER;
|
|
5
6
|
api: CreateExamSdkApi;
|
|
6
7
|
listPath?: string;
|
|
7
8
|
onNavigate?: (href: string) => void;
|
|
9
|
+
/** Optional label overrides (e.g. to reuse this flow for homework). */
|
|
10
|
+
texts?: CreateExamTexts;
|
|
8
11
|
}
|
|
9
12
|
export declare function CreateExamPageContainer(props: CreateExamPageContainerProps): import("react").JSX.Element;
|
|
@@ -1,103 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { Suspense, useEffect, useState } from 'react';
|
|
4
|
-
import { zodResolver } from '@hookform/resolvers/zod';
|
|
5
|
-
import { useForm } from 'react-hook-form';
|
|
6
|
-
import { ArrowLeft, ArrowRight, LayoutTemplate, Loader2, X } from 'lucide-react';
|
|
7
|
-
import { Button } from '../../components/ui/button';
|
|
8
|
-
import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
|
|
9
|
-
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../components/ui/dialog';
|
|
10
|
-
import { useToast } from '../../shared/lib/hooks/useToast';
|
|
11
|
-
import { useT } from '../../shared/lib/i18n';
|
|
12
|
-
import { buildExamTemplatePayload } from '../../api/exam-create';
|
|
13
|
-
import { CreateExamInfoForm } from './CreateExamInfoForm';
|
|
14
|
-
import { TemplateSelector } from './TemplateSelector';
|
|
15
|
-
import { examFormSchema } from './create-exam.validation';
|
|
16
|
-
function CreateExamContent({ api, listPath = '/manage/exams', onNavigate }) {
|
|
17
|
-
const t = useT();
|
|
18
|
-
const { success, warning, error: errorToast } = useToast();
|
|
19
|
-
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false);
|
|
20
|
-
const [selectedTemplate, setSelectedTemplate] = useState(null);
|
|
21
|
-
const [examLevels, setExamLevels] = useState([]);
|
|
22
|
-
const [isLoadingExamLevels, setIsLoadingExamLevels] = useState(false);
|
|
23
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
24
|
-
const form = useForm({
|
|
25
|
-
resolver: zodResolver(examFormSchema),
|
|
26
|
-
mode: 'onBlur',
|
|
27
|
-
defaultValues: { code: '', name: '', level: '', difficulty: '', duration: '', totalPoints: '100', description: '' },
|
|
28
|
-
});
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
if (!api.getExamLevels)
|
|
31
|
-
return;
|
|
32
|
-
let ignore = false;
|
|
33
|
-
setIsLoadingExamLevels(true);
|
|
34
|
-
api.getExamLevels()
|
|
35
|
-
.then((levels) => { if (!ignore)
|
|
36
|
-
setExamLevels(levels); })
|
|
37
|
-
.catch((err) => errorToast({ message: t('admin.exams.questions.error'), description: err instanceof Error ? err.message : undefined }))
|
|
38
|
-
.finally(() => { if (!ignore)
|
|
39
|
-
setIsLoadingExamLevels(false); });
|
|
40
|
-
return () => { ignore = true; };
|
|
41
|
-
}, [api, errorToast, t]);
|
|
42
|
-
const navigate = (href) => {
|
|
43
|
-
if (onNavigate)
|
|
44
|
-
onNavigate(href);
|
|
45
|
-
else if (typeof window !== 'undefined')
|
|
46
|
-
window.location.href = href;
|
|
47
|
-
};
|
|
48
|
-
const handleSubmit = form.handleSubmit(async (data) => {
|
|
49
|
-
if (!selectedTemplate) {
|
|
50
|
-
warning({ message: t('admin.exams.create.incompleteInfo'), description: 'Vui lòng chọn template đề thi trước khi tiếp tục.' });
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
setIsSaving(true);
|
|
54
|
-
try {
|
|
55
|
-
const exam = unwrap(await api.createExam({
|
|
56
|
-
name: data.name,
|
|
57
|
-
code: data.code || undefined,
|
|
58
|
-
examLevel: data.level || undefined,
|
|
59
|
-
difficulty: toDifficulty(data.difficulty),
|
|
60
|
-
totalScore: Number.parseInt(data.totalPoints, 10),
|
|
61
|
-
durationMinutes: data.duration ? Number.parseInt(data.duration, 10) : undefined,
|
|
62
|
-
description: data.description || undefined,
|
|
63
|
-
isComplete: false,
|
|
64
|
-
isActive: false,
|
|
65
|
-
examTemplateId: selectedTemplate.id,
|
|
66
|
-
}));
|
|
67
|
-
await api.updateExamTemplate({ examId: exam.id, template: buildExamTemplatePayload(selectedTemplate) });
|
|
68
|
-
success({ message: t('admin.exams.create.success'), description: t('admin.exams.create.successDesc', { name: exam.name }) });
|
|
69
|
-
navigate(`${listPath}/${exam.id}/questions`);
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
errorToast({ message: t('admin.exams.questions.error'), description: err instanceof Error ? err.message : undefined });
|
|
73
|
-
}
|
|
74
|
-
finally {
|
|
75
|
-
setIsSaving(false);
|
|
76
|
-
}
|
|
77
|
-
}, () => warning({ message: t('admin.exams.create.incompleteInfo'), description: t('admin.exams.create.incompleteInfoDesc') }));
|
|
78
|
-
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx(Button, { variant: "ghost", size: "icon", onClick: () => navigate(listPath), className: "h-8 w-8", children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold tracking-tight text-gray-900", children: t('admin.exams.create.title') }), _jsx("p", { className: "mt-1 text-gray-600", children: t('admin.exams.create.step1') })] })] }), _jsx("form", { id: "exam-form", onSubmit: handleSubmit, children: _jsx(CreateExamInfoForm, { form: form, examLevels: examLevels, isLoadingExamLevels: isLoadingExamLevels }) }), _jsx(TemplateSection, { selectedTemplate: selectedTemplate, onOpen: () => setIsTemplateDialogOpen(true), onClear: () => setSelectedTemplate(null) }), _jsx(Dialog, { open: isTemplateDialogOpen, onOpenChange: setIsTemplateDialogOpen, children: _jsxs(DialogContent, { className: "flex max-h-[92vh] min-h-[92vh] max-w-5xl flex-col overflow-hidden", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Ch\u1ECDn template \u0111\u1EC1 thi" }), _jsx(DialogDescription, { children: "Ch\u1ECDn lo\u1EA1i \u0111\u1EC1 thi b\u1EA1n mu\u1ED1n t\u1EA1o." })] }), _jsx(TemplateSelector, { api: api, onTemplateSelect: (template) => { setSelectedTemplate(template); setIsTemplateDialogOpen(false); }, onCancel: () => setIsTemplateDialogOpen(false) })] }) }), _jsxs("div", { className: "flex justify-end gap-3", children: [_jsx(Button, { variant: "outline", onClick: () => navigate(listPath), disabled: isSaving, children: t('admin.exams.create.cancel') }), _jsx(Button, { type: "submit", form: "exam-form", className: "bg-blue-600 hover:bg-blue-700", disabled: isSaving || !selectedTemplate, children: isSaving ? _jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), t('admin.exams.create.saving')] }) : _jsxs(_Fragment, { children: [t('admin.exams.create.saveAndContinue'), _jsx(ArrowRight, { className: "ml-2 h-4 w-4" })] }) })] })] }));
|
|
79
|
-
}
|
|
80
|
-
export function CreateExamPageContainer(props) {
|
|
81
|
-
return _jsx(Suspense, { fallback: _jsx(Loading, {}), children: _jsx(CreateExamContent, { ...props }) });
|
|
82
|
-
}
|
|
83
|
-
function TemplateSection({ selectedTemplate, onOpen, onClear }) {
|
|
84
|
-
return _jsxs(Card, { className: "border border-gray-200", children: [_jsx(CardHeader, { className: "border-b border-gray-100 bg-gray-50/50", children: _jsxs(CardTitle, { className: "flex items-center gap-2 text-base font-semibold", children: [_jsx(LayoutTemplate, { className: "h-4 w-4 text-blue-600" }), "Ch\u1ECDn Template ", _jsx("span", { className: "text-red-500", children: "*" })] }) }), _jsx(CardContent, { className: "pt-6", children: selectedTemplate ? _jsxs("div", { className: "flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-green-800", children: selectedTemplate.name }), _jsxs("p", { className: "text-xs text-green-600", children: [selectedTemplate.totalQuestions, " c\u00E2u h\u1ECFi \u2022 ", selectedTemplate.parts.length, " ph\u1EA7n"] })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onOpen, children: "\u0110\u1ED5i template" }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: onClear, children: _jsx(X, { className: "h-3.5 w-3.5" }) })] })] }) : _jsxs("div", { className: "flex flex-col items-center gap-3 rounded-lg border-2 border-dashed border-gray-300 py-8", children: [_jsx(LayoutTemplate, { className: "h-8 w-8 text-gray-400" }), _jsx("p", { className: "text-sm text-gray-500", children: "Ch\u01B0a ch\u1ECDn template \u0111\u1EC1 thi" }), _jsxs(Button, { type: "button", variant: "outline", onClick: onOpen, children: [_jsx(LayoutTemplate, { className: "mr-2 h-4 w-4" }), "Ch\u1ECDn template"] })] }) })] });
|
|
85
|
-
}
|
|
86
|
-
function Loading() {
|
|
87
|
-
return _jsx("div", { className: "flex h-screen items-center justify-center", children: _jsx(Loader2, { className: "h-8 w-8 animate-spin text-blue-600" }) });
|
|
88
|
-
}
|
|
89
|
-
function toDifficulty(value) {
|
|
90
|
-
if (value === 'easy')
|
|
91
|
-
return 'EASY';
|
|
92
|
-
if (value === 'medium')
|
|
93
|
-
return 'MEDIUM';
|
|
94
|
-
if (value === 'hard')
|
|
95
|
-
return 'HARD';
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
function unwrap(value) {
|
|
99
|
-
return hasData(value) ? value.data : value;
|
|
100
|
-
}
|
|
101
|
-
function hasData(value) {
|
|
102
|
-
return typeof value === 'object' && value !== null && 'data' in value;
|
|
103
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t,Fragment as a}from"react/jsx-runtime";import{Suspense as n,useEffect as i,useState as r}from"react";import{zodResolver as s}from"@hookform/resolvers/zod";import{useForm as l}from"react-hook-form";import{ArrowLeft as o,ArrowRight as m,LayoutTemplate as c,Loader2 as d,X as p}from"lucide-react";import{Button as h}from"../../components/ui/button";import{Card as u,CardContent as x,CardHeader as f,CardTitle as g}from"../../components/ui/card";import{Dialog as b,DialogContent as v,DialogDescription as N,DialogHeader as y,DialogTitle as w}from"../../components/ui/dialog";import{useToast as C}from"../../shared/lib/hooks/useToast";import{useT as T}from"../../shared/lib/i18n";import{buildExamTemplatePayload as E}from"../../api/exam-create";import{CreateExamInfoForm as k}from"./CreateExamInfoForm";import{TemplateSelector as D}from"./TemplateSelector";import{examFormSchema as L}from"./create-exam.validation";function I({api:n,listPath:c="/manage/exams",onNavigate:p,texts:u}){const x=T(),{success:f,warning:g,error:I}=C(),[S,M]=r(!1),[z,P]=r(null),[q,O]=r([]),[F,H]=r(!1),[R,U]=r(!1),V=l({resolver:s(L),mode:"onBlur",defaultValues:{code:"",name:"",level:"",difficulty:"",duration:"",totalPoints:"100",description:""}});i(()=>{if(!n.getExamLevels)return;let e=!1;return H(!0),n.getExamLevels().then(t=>{e||O(t)}).catch(e=>I({message:x("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})).finally(()=>{e||H(!1)}),()=>{e=!0}},[n,I,x]);const Y=e=>{p?p(e):"undefined"!=typeof window&&(window.location.href=e)},$=V.handleSubmit(async e=>{if(z){U(!0);try{const a=function(e){return"object"==typeof e&&null!==e&&"data"in e}(t=await n.createExam({name:e.name,code:e.code||void 0,examLevel:e.level||void 0,difficulty:A(e.difficulty),totalScore:Number.parseInt(e.totalPoints,10),durationMinutes:e.duration?Number.parseInt(e.duration,10):void 0,description:e.description||void 0,isComplete:!1,isActive:!1,examTemplateId:z.id}))?t.data:t;await n.updateExamTemplate({examId:a.id,template:E(z)}),f({message:x("admin.exams.create.success"),description:u?.successDescription?.replace("{name}",a.name)??x("admin.exams.create.successDesc",{name:a.name})}),Y(`${c}/${a.id}/questions`)}catch(e){I({message:x("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})}finally{U(!1)}var t}else g({message:x("admin.exams.create.incompleteInfo"),description:u?.missingTemplateDescription??"Vui lòng chọn template đề thi trước khi tiếp tục."})},()=>g({message:x("admin.exams.create.incompleteInfo"),description:x("admin.exams.create.incompleteInfoDesc")}));return t("div",{className:"space-y-6",children:[t("div",{className:"flex items-center gap-4",children:[e(h,{variant:"ghost",size:"icon",onClick:()=>Y(c),className:"h-8 w-8",children:e(o,{className:"h-4 w-4"})}),t("div",{children:[e("h1",{className:"text-3xl font-bold tracking-tight text-gray-900",children:u?.title??x("admin.exams.create.title")}),e("p",{className:"mt-1 text-gray-600",children:u?.step1??x("admin.exams.create.step1")})]})]}),e("form",{id:"exam-form",onSubmit:$,children:e(k,{form:V,examLevels:q,isLoadingExamLevels:F,texts:u})}),e(j,{selectedTemplate:z,onOpen:()=>M(!0),onClear:()=>P(null),texts:u}),e(b,{open:S,onOpenChange:M,children:t(v,{className:"flex max-h-[92vh] min-h-[92vh] max-w-5xl flex-col overflow-hidden",children:[t(y,{children:[e(w,{children:u?.templateDialogTitle??"Chọn template đề thi"}),e(N,{children:u?.templateDialogDescription??"Chọn loại đề thi bạn muốn tạo."})]}),e(D,{api:n,onTemplateSelect:e=>{P(e),M(!1)},onCancel:()=>M(!1)})]})}),t("div",{className:"flex justify-end gap-3",children:[e(h,{variant:"outline",onClick:()=>Y(c),disabled:R,children:u?.cancelLabel??x("admin.exams.create.cancel")}),e(h,{type:"submit",form:"exam-form",className:"bg-blue-600 hover:bg-blue-700",disabled:R||!z,children:t(a,R?{children:[e(d,{className:"mr-2 h-4 w-4 animate-spin"}),u?.savingLabel??x("admin.exams.create.saving")]}:{children:[u?.saveAndContinueLabel??x("admin.exams.create.saveAndContinue"),e(m,{className:"ml-2 h-4 w-4"})]})})]})]})}export function CreateExamPageContainer(t){return e(n,{fallback:e(S,{}),children:e(I,{...t})})}function j({selectedTemplate:a,onOpen:n,onClear:i,texts:r}){return t(u,{className:"border border-gray-200",children:[e(f,{className:"border-b border-gray-100 bg-gray-50/50",children:t(g,{className:"flex items-center gap-2 text-base font-semibold",children:[e(c,{className:"h-4 w-4 text-blue-600"}),r?.templateSectionTitle??"Chọn Template"," ",e("span",{className:"text-red-500",children:"*"})]})}),e(x,{className:"pt-6",children:t("div",a?{className:"flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-green-100",children:e(c,{className:"h-4 w-4 text-green-600"})}),t("div",{children:[e("p",{className:"text-sm font-medium text-green-800",children:a.name}),t("p",{className:"text-xs text-green-600",children:[a.totalQuestions," câu hỏi • ",a.parts.length," phần"]})]})]}),t("div",{className:"flex items-center gap-2",children:[e(h,{type:"button",variant:"outline",size:"sm",onClick:n,className:"border-green-300 text-green-700 hover:bg-green-100",children:r?.changeTemplateLabel??"Đổi template"}),e(h,{type:"button",variant:"ghost",size:"icon",onClick:i,className:"h-7 w-7 text-green-600 hover:bg-green-100 hover:text-green-800",children:e(p,{className:"h-3.5 w-3.5"})})]})]}:{className:"flex flex-col items-center gap-3 rounded-lg border-2 border-dashed border-gray-300 py-8",children:[e(c,{className:"h-8 w-8 text-gray-400"}),e("p",{className:"text-sm text-gray-500",children:r?.noTemplateText??"Chưa chọn template đề thi"}),t(h,{type:"button",variant:"outline",onClick:n,children:[e(c,{className:"mr-2 h-4 w-4"}),r?.selectTemplateLabel??"Chọn template"]})]})})]})}function S(){return e("div",{className:"flex h-screen items-center justify-center",children:e(d,{className:"h-8 w-8 animate-spin text-blue-600"})})}function A(e){return"EASY"===e?"EASY":"MEDIUM"===e?"MEDIUM":"HARD"===e?"HARD":void 0}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type ExistingQuestionBank } from '../questions/components/QuestionBankOption';
|
|
1
2
|
import type { ExamData, ExamQuestion, ExamTemplate } from '../../shared/types/entities/exam.types';
|
|
2
|
-
import type { SaveQuestionInput } from '../../api/exam-questions.types';
|
|
3
|
+
import type { SaveQuestionInput, SearchQuestionBankInput, SearchQuestionBankResult } from '../../api/exam-questions.types';
|
|
3
4
|
export interface ExamCreatorProps {
|
|
4
5
|
template: ExamTemplate;
|
|
5
6
|
examId?: string;
|
|
@@ -7,10 +8,15 @@ export interface ExamCreatorProps {
|
|
|
7
8
|
examLevel?: string;
|
|
8
9
|
subjectId?: string;
|
|
9
10
|
initialQuestions?: Partial<ExamQuestion>[];
|
|
10
|
-
onSaveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> |
|
|
11
|
+
onSaveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | {
|
|
12
|
+
question: Partial<ExamQuestion>;
|
|
13
|
+
questionBank?: ExistingQuestionBank | null;
|
|
14
|
+
} | void>;
|
|
11
15
|
onSave?: (examData: ExamData) => void | Promise<void>;
|
|
12
16
|
onCancel?: () => void;
|
|
13
17
|
onPartAudioChange?: (partId: string, audioUrl: string) => void | Promise<void>;
|
|
14
18
|
partAudioOverrides?: Record<string, string>;
|
|
19
|
+
searchQuestions?: (input: SearchQuestionBankInput) => Promise<SearchQuestionBankResult>;
|
|
20
|
+
saveExamRef?: React.MutableRefObject<(() => void) | null>;
|
|
15
21
|
}
|
|
16
|
-
export declare function ExamCreator(
|
|
22
|
+
export declare function ExamCreator(props: ExamCreatorProps): import("react").JSX.Element;
|
|
@@ -1,89 +1 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { ChevronLeft, ChevronRight, Save } from 'lucide-react';
|
|
5
|
-
import { Button } from '../../components/ui/button';
|
|
6
|
-
import { QuestionCreator } from '../questions/creator/QuestionCreator';
|
|
7
|
-
import { useToast } from '../../shared/lib/hooks/useToast';
|
|
8
|
-
import { useT } from '../../shared/lib/i18n';
|
|
9
|
-
import { ExamPartTabs } from './ExamPartTabs';
|
|
10
|
-
import { ExamQuestionGrid } from './ExamQuestionGrid';
|
|
11
|
-
import { PartAudioUploader } from './PartAudioUploader';
|
|
12
|
-
import { getEffectiveQuestionConfig, getPartByQuestionIndex, normalizeTemplateGroupConfig } from './exam-template.utils';
|
|
13
|
-
export function ExamCreator({ template, examId, examDifficulty, examLevel, subjectId, initialQuestions = [], onSaveQuestion, onSave, onCancel, onPartAudioChange, partAudioOverrides }) {
|
|
14
|
-
const t = useT();
|
|
15
|
-
const { success, warning, error } = useToast();
|
|
16
|
-
const validationRef = useRef(null);
|
|
17
|
-
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
|
18
|
-
const [questions, setQuestions] = useState(() => Object.fromEntries(initialQuestions.map((q) => [(q.questionNumber ?? 1) - 1, q])));
|
|
19
|
-
const [completedQuestions, setCompletedQuestions] = useState(() => new Set(initialQuestions.map((q) => (q.questionNumber ?? 1) - 1)));
|
|
20
|
-
const [validationErrors, setValidationErrors] = useState({});
|
|
21
|
-
const [isSavingQuestion, setIsSavingQuestion] = useState(false);
|
|
22
|
-
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
23
|
-
const draftRef = useRef({});
|
|
24
|
-
const currentPart = getPartByQuestionIndex(template, currentQuestionIndex);
|
|
25
|
-
const normalizedPart = currentPart ? normalizeTemplateGroupConfig(currentPart) : undefined;
|
|
26
|
-
const currentConfig = currentPart ? getEffectiveQuestionConfig(currentPart, currentQuestionIndex) : undefined;
|
|
27
|
-
const isPartLevelGroup = !!(normalizedPart?.groups && !currentConfig?.groups);
|
|
28
|
-
const sectionName = currentConfig && 'sectionName' in currentConfig ? currentConfig.sectionName : undefined;
|
|
29
|
-
const sharedDataPartId = sectionName && !isPartLevelGroup ? `${currentPart?.id}_${sectionName}` : currentPart?.id;
|
|
30
|
-
const currentQuestion = questions[currentQuestionIndex];
|
|
31
|
-
const examData = useMemo(() => ({
|
|
32
|
-
examId,
|
|
33
|
-
templateId: template.id,
|
|
34
|
-
questions: Object.values(questions),
|
|
35
|
-
currentQuestionIndex,
|
|
36
|
-
lastSaved: new Date().toISOString(),
|
|
37
|
-
createdAt: new Date().toISOString(),
|
|
38
|
-
updatedAt: new Date().toISOString(),
|
|
39
|
-
isCompleted: completedQuestions.size >= template.totalQuestions,
|
|
40
|
-
}), [completedQuestions.size, currentQuestionIndex, examId, questions, template.id, template.totalQuestions]);
|
|
41
|
-
const navigateToQuestion = (index) => {
|
|
42
|
-
if (hasUnsavedChanges)
|
|
43
|
-
warning({ message: t('common.examCreator.saveBeforeExit') });
|
|
44
|
-
setCurrentQuestionIndex(Math.max(0, Math.min(template.totalQuestions - 1, index)));
|
|
45
|
-
setHasUnsavedChanges(false);
|
|
46
|
-
draftRef.current = {};
|
|
47
|
-
};
|
|
48
|
-
const handleSaveQuestion = async (questionData) => {
|
|
49
|
-
if (!currentPart)
|
|
50
|
-
return;
|
|
51
|
-
const valid = await validationRef.current?.triggerValidation?.();
|
|
52
|
-
if (valid === false)
|
|
53
|
-
return;
|
|
54
|
-
const question = {
|
|
55
|
-
...currentQuestion,
|
|
56
|
-
...draftRef.current,
|
|
57
|
-
...questionData,
|
|
58
|
-
type: questionData?.type ?? draftRef.current.type ?? currentQuestion?.type ?? currentConfig?.questionType,
|
|
59
|
-
partId: currentPart.id,
|
|
60
|
-
questionNumber: currentQuestionIndex + 1,
|
|
61
|
-
difficulty: (questionData?.difficulty ?? examDifficulty ?? currentQuestion?.difficulty),
|
|
62
|
-
skill: currentPart.skill,
|
|
63
|
-
isCompleted: true,
|
|
64
|
-
};
|
|
65
|
-
setIsSavingQuestion(true);
|
|
66
|
-
try {
|
|
67
|
-
const saved = await onSaveQuestion?.({ question, questionIndex: currentQuestionIndex, template, examId, examDifficulty, examLevel, subjectId });
|
|
68
|
-
const nextQuestion = (saved || question);
|
|
69
|
-
setQuestions((prev) => ({ ...prev, [currentQuestionIndex]: nextQuestion }));
|
|
70
|
-
setCompletedQuestions((prev) => new Set(prev).add(currentQuestionIndex));
|
|
71
|
-
setHasUnsavedChanges(false);
|
|
72
|
-
success({ message: t('common.examCreator.saveSuccess', { index: String(currentQuestionIndex + 1) }) });
|
|
73
|
-
}
|
|
74
|
-
catch (err) {
|
|
75
|
-
error({ message: t('common.examCreator.saveError', { index: String(currentQuestionIndex + 1) }), description: err instanceof Error ? err.message : undefined });
|
|
76
|
-
}
|
|
77
|
-
finally {
|
|
78
|
-
setIsSavingQuestion(false);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
const handleSaveExam = async () => {
|
|
82
|
-
if (hasUnsavedChanges) {
|
|
83
|
-
warning({ message: t('common.examCreator.saveBeforeExit') });
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
await onSave?.(examData);
|
|
87
|
-
};
|
|
88
|
-
return (_jsx("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50/30", children: _jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "w-80 overflow-y-auto border-r border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm", children: _jsx(ExamPartTabs, { template: template, currentPartId: currentPart?.id ?? '', completedQuestions: completedQuestions, onPartChange: (partId) => navigateToQuestion(template.parts.find((part) => part.id === partId)?.startIndex ?? 0) }) }), _jsxs("div", { className: "flex flex-1 flex-col overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-4 pb-4", children: _jsxs("div", { className: "mx-auto max-w-4xl", children: [currentPart?.skill === 'LISTENING' && _jsx(PartAudioUploader, { partId: currentPart.id, partName: currentPart.name, audioUrl: partAudioOverrides?.[currentPart.id] ?? currentPart.audioUrl ?? '', examId: examId, onAudioChange: (url) => void onPartAudioChange?.(currentPart.id, url), isReadOnly: !onPartAudioChange }), _jsx(QuestionCreator, { initialData: currentQuestion, prefilledType: currentConfig?.questionType, prefilledSkill: currentPart?.skill, optionCount: currentConfig?.optionCount, optionMode: currentConfig?.optionMode, defaultValue: currentConfig?.defaultValue, defaultQuestion: currentConfig?.defaultQuestion, viewMode: currentConfig?.viewMode, groups: currentConfig?.groups || normalizedPart?.groups, groupConfig: currentConfig?.groupConfig || normalizedPart?.groupConfig, groupType: currentConfig?.groupType || normalizedPart?.groupType, articleCount: currentConfig?.articleCount ?? currentPart?.articleCount, questionConfig: currentConfig?.questionConfig || currentPart?.questionConfig, questionIndexInPart: isPartLevelGroup ? currentQuestionIndex - (currentPart?.startIndex ?? 0) : currentQuestionIndex - (currentConfig?.startIndex ?? currentPart?.startIndex ?? 0), partId: sharedDataPartId, onChange: (draft) => { draftRef.current = draft; setHasUnsavedChanges(true); }, onSave: handleSaveQuestion, onCancel: onCancel, isEmbedded: true, externalErrors: validationErrors[currentQuestionIndex], validationRef: validationRef, suggestedAnswers: currentConfig?.suggestedAnswers || currentPart?.suggestedAnswers, hasSuggestedAnswers: currentConfig?.hasSuggestedAnswers ?? currentPart?.hasSuggestedAnswers }, currentQuestionIndex)] }) }), _jsx("div", { className: "flex-shrink-0 border-t border-gray-200/80 bg-white/95 shadow-[0_-4px_20px_rgba(0,0,0,0.08)] backdrop-blur-sm", children: _jsxs("div", { className: "mx-auto flex max-w-4xl items-center justify-between px-6 py-4", children: [_jsxs(Button, { variant: "outline", onClick: () => navigateToQuestion(currentQuestionIndex - 1), disabled: currentQuestionIndex === 0, children: [_jsx(ChevronLeft, { className: "mr-2 h-4 w-4" }), "C\u00E2u tr\u01B0\u1EDBc"] }), _jsxs(Button, { onClick: () => void handleSaveQuestion(), disabled: isSavingQuestion, className: "bg-green-600 hover:bg-green-700", children: [_jsx(Save, { className: "mr-2 h-4 w-4" }), isSavingQuestion ? 'Đang lưu...' : 'Lưu câu hỏi'] }), _jsxs(Button, { variant: "outline", onClick: () => navigateToQuestion(currentQuestionIndex + 1), disabled: currentQuestionIndex >= template.totalQuestions - 1, children: ["C\u00E2u ti\u1EBFp", _jsx(ChevronRight, { className: "ml-2 h-4 w-4" })] })] }) })] }), _jsxs("div", { className: "w-80 overflow-y-auto border-l border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm", children: [_jsx(ExamQuestionGrid, { template: template, currentQuestionIndex: currentQuestionIndex, completedQuestions: completedQuestions, onQuestionChange: navigateToQuestion }), _jsxs(Button, { onClick: () => void handleSaveExam(), className: "mt-4 w-full bg-blue-600 hover:bg-blue-700", children: [_jsx(Save, { className: "mr-2 h-4 w-4" }), "Ho\u00E0n t\u1EA5t \u0111\u1EC1 thi"] })] })] }) }));
|
|
89
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t,Fragment as n}from"react/jsx-runtime";import{useCallback as o,useEffect as i,useMemo as r,useRef as a,useState as s}from"react";import{CheckCircle as d,ChevronLeft as l,ChevronRight as u,Loader2 as c,Save as m}from"lucide-react";import{Button as p}from"../../components/ui/button";import{QuestionBankOption as g}from"../questions/components/QuestionBankOption";import{QuestionSearchDropdown as f}from"../questions/components/QuestionSearchDropdown";import{QuestionCreator as h}from"../questions/creator/QuestionCreator";import{useToast as x}from"../../shared/lib/hooks/useToast";import{useT as b}from"../../shared/lib/i18n";import{useExamQuestionStore as v}from"../../shared/lib/stores/examQuestionStore";import{transformApiQuestionToFrontend as C}from"../../shared/lib/utils/question-reverse-transform";import{ExamPartTabs as w}from"./ExamPartTabs";import{ExamQuestionGrid as y}from"./ExamQuestionGrid";import{PartAudioUploader as N}from"./PartAudioUploader";import{CompletionDialog as I}from"./CompletionDialog";import{UnsavedChangesDialog as k}from"./UnsavedChangesDialog";import{clearQuestionStatuses as q,getQuestionStatus as Q,QuestionStatus as S,setQuestionStatus as E}from"./exam-question-status";import{getEffectiveQuestionConfig as D,getPartByQuestionIndex as T,normalizeTemplateGroupConfig as A}from"./exam-template.utils";export function ExamCreator(L){const{template:B,examId:V=`exam-${Date.now()}`,initialQuestions:j=[]}=L,U=b(),{success:P,warning:$,error:M}=x(),R=a(null),z=a({}),F=a(!1),G=a(!1),H=a(!1),K=v(e=>e.initialized),W=v(e=>e.examId),J=v(e=>e.initializeExam),X=v(e=>e.resetStore),Y=v(e=>e.currentQuestionIndex),Z=v(e=>e.setCurrentQuestionIndex),ee=v(e=>e.questions),te=v(e=>e.upsertQuestionData),ne=v(e=>e.clearQuestionData),oe=v(e=>e.markQuestionCompleted),ie=v(e=>e.getQuestionData),re=v(e=>e.setSharedHintLetters),[ae,se]=s({}),[de,le]=s(!1),[ue,ce]=s(!1),[me,pe]=s(!1),[ge,fe]=s(null),[he,xe]=s(!1),[be,ve]=s(!1),[Ce,we]=s({}),[ye,Ne]=s({}),[Ie,ke]=s({}),[qe,Qe]=s(),[Se,Ee]=s(),De=T(B,Y),Te=De?.id??"",Ae=De?D(De,Y):void 0,Oe=De?A(De):void 0,_e=Boolean(Oe?.groups&&!Ae?.groups),Le=_(Ae),Be=Le&&!_e?`${Te}_${Le}`:Te,Ve=ee[Y]?.data,je=Ce[Y]??{checked:!1,code:"",title:""},Ue=r(()=>{const e=new Set;return Object.entries(ee).forEach(([t,n])=>{n.isCompleted&&e.add(Number(t))}),e},[ee]),Pe=r(()=>({id:V,templateId:B.id,questions:Object.values(ee).map(e=>e.data),currentQuestionIndex:Y,lastSaved:(new Date).toISOString(),createdAt:(new Date).toISOString(),updatedAt:(new Date).toISOString(),isCompleted:Ue.size>=B.totalQuestions}),[Ue.size,Y,V,ee,B.id,B.totalQuestions]);i(()=>{const e=W&&W!==V;e&&X(),K&&!e||J({examId:V,templateId:B.id,totalQuestions:B.totalQuestions})},[V,J,K,X,W,B.id,B.totalQuestions]),i(()=>{if(!K||0===j.length)return;F.current=!0,j.forEach(e=>{const t=(e.questionNumber??1)-1,n={...e,isCompleted:!0};te(t,n),oe(t,!0),z.current[t]=n,function(e,t,n,o){if("LOOK_PICTURE_FILL_WORD_HINT"!==t.type||!t.partId)return;const i=t.answer,r=t.questionGroup?.groupNumber;if(!i?.hintLetters?.length||!r)return;const a=T(n,e),s=a?D(a,e):void 0,d=_(s),l=d?`${t.partId}_${d}`:t.partId;o(l,r,i.hintLetters)}(t,n,B,re);const o=e.questionBank;o?.id&&Ne(e=>({...e,[t]:o}))});const e=window.setTimeout(()=>{F.current=!1},1e3);return()=>window.clearTimeout(e)},[j,K,oe,re,B,te]),i(()=>{pe(Q(V,Y+1)===S.UNSAVED)},[Y,V]),i(()=>{ce(Object.values(ee).some(e=>Object.keys(e.data).length>0&&!e.isCompleted))},[ee]),i(()=>{const e=e=>{ue&&(e.preventDefault(),e.returnValue="")};return window.addEventListener("beforeunload",e),()=>window.removeEventListener("beforeunload",e)},[ue]);const $e=o(e=>{H.current=!0,pe(!1),Z(Math.max(0,Math.min(B.totalQuestions-1,e))),window.setTimeout(()=>{H.current=!1},500)},[Z,B.totalQuestions]),Me=e=>{if(me)return fe(e),void xe(!0);$e(e)},Re=o(e=>{F.current||G.current||H.current||(te(Y,{questionNumber:Y+1,partId:Te,...e}),E(V,Y+1,S.UNSAVED),pe(!0))},[Te,Y,V,te]),ze=async e=>{if(!De)return;if(!function(e,t,n,o){if(!e.checked||t?.id)return!0;const i=e.code.trim()?void 0:"Vui lòng nhập mã câu hỏi cho ngân hàng",r=e.title.trim()?void 0:"Vui lòng nhập tiêu đề câu hỏi cho ngân hàng";return n(i),o(r),!i&&!r}(je,ye[Y],Qe,Ee))return;if(!1===await(R.current?.triggerValidation?.()))return;const t=R.current?.getFormData?.(),n=function(e,t,n,o,i,r,a){return{...n,...e,...t?{answer:t}:{},type:e?.type??n?.type??o,id:n?.id??e?.id,partId:i.id,questionNumber:r+1,difficulty:e?.difficulty??a??n?.difficulty,skill:i.skill,isCompleted:!0}}(e,t,ie(Y),Ae?.questionType,De,Y,L.examDifficulty);le(!0);try{const e=await(L.onSaveQuestion?.({question:n,questionIndex:Y,template:B,examId:V,examDifficulty:L.examDifficulty,examLevel:L.examLevel,subjectId:L.subjectId,sectionId:(o=Ae,o&&"sectionId"in o?o.sectionId:void 0),addToQuestionBank:!ye[Y]?.id&&je.checked,questionBankCode:je.checked?je.code:void 0,questionBankTitle:je.checked?je.title:void 0,existingQuestionBankId:ye[Y]?.id})),t=function(e,t){return e&&"question"in e?{...t,...e.question,isCompleted:!0}:{...t,...e??{},isCompleted:!0}}(e,n);te(Y,t),oe(Y,!0),z.current[Y]=t,function(e,t,n){const o=e&&"questionBank"in e?e.questionBank:e?.questionBank;o?.id&&n(e=>({...e,[t]:o}))}(e,Y,Ne),G.current=!0,window.setTimeout(()=>{G.current=!1},500),E(V,Y+1,S.SAVED),pe(!1),P({message:U("common.examCreator.saveSuccess",{index:String(Y+1)})})}catch(e){M({message:U("common.examCreator.saveError",{index:String(Y+1)}),description:e instanceof Error?e.message:void 0})}finally{le(!1)}var o};return L.saveExamRef&&(L.saveExamRef.current=()=>{me?$({message:U("common.examCreator.saveBeforeExit")}):ve(!0)}),t("div",{className:"flex min-h-0 flex-1 flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50/30",children:[t("div",{className:"flex min-h-0 flex-1 overflow-hidden",children:[e("div",{className:"w-80 overflow-y-auto border-r border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(w,{template:B,currentPartId:Te,completedQuestions:Ue,onPartChange:e=>Me(B.parts.find(t=>t.id===e)?.startIndex??0)})}),t("div",{className:"flex flex-1 flex-col overflow-hidden",children:[e("div",{className:"min-h-0 flex-1 overflow-y-auto p-4 pb-4",children:t("div",{className:"mx-auto max-w-4xl",children:["LISTENING"===De?.skill&&e(N,{partId:De.id,partName:De.name,audioUrl:L.partAudioOverrides?.[De.id]??De.audioUrl??"",examId:V,onAudioChange:e=>{L.onPartAudioChange?.(De.id,e)},isReadOnly:!L.onPartAudioChange}),e(h,{initialData:Ve,prefilledType:Ae?.questionType,prefilledSkill:De?.skill,optionCount:Ae?.optionCount,optionMode:Ae?.optionMode,defaultValue:Ae?.defaultValue,defaultQuestion:Ae?.defaultQuestion,viewMode:Ae?.viewMode,groups:Ae?.groups||Oe?.groups,groupConfig:Ae?.groupConfig||Oe?.groupConfig,groupType:Ae?.groupType||Oe?.groupType,articleCount:Ae?.articleCount??De?.articleCount,questionConfig:Ae?.questionConfig||De?.questionConfig,questionIndexInPart:_e?Y-(De?.startIndex??0):Y-(Ae?.startIndex??De?.startIndex??0),partId:Be,onChange:Re,onUnsavedChangesChange:pe,onSave:ze,onCancel:L.onCancel,isEmbedded:!0,externalErrors:ae[Y],validationRef:R,suggestedAnswers:Ae?.suggestedAnswers||De?.suggestedAnswers,hasSuggestedAnswers:Ae?.hasSuggestedAnswers??De?.hasSuggestedAnswers,headerExtra:e(f,{questionType:Ae?.questionType||De?.questionType,onSelect:e=>{const t={...C({question_type:e.type,content:e.content,correct_answer:e.answer,points:e.points,difficult_level:e.difficulty,question_skill_type:e.skill,explanation:e.explanation}),id:void 0,partId:Te,questionNumber:Y+1,isCompleted:!1};te(Y,t);const n=e.id,o=e.code,i=e.title;(o||i)&&ke(e=>({...e,[Y]:{code:o,title:i}})),n&&(o||i)&&Ne(e=>({...e,[Y]:{id:n,code:o??"",title:i??""}})),E(V,Y+1,S.UNSAVED),pe(!0),oe(Y,!1),P({message:U("common.examCreator.bankSelected")})},selectedQuestionLabel:Ie[Y]?`${Ie[Y].code?`#${Ie[Y].code} - `:""}${Ie[Y].title||""}`:void 0,disabled:!(Ae?.questionType||De?.questionType),compact:!0,searchQuestions:L.searchQuestions})},Y),e("div",{className:"mt-8",children:e(g,{checked:je.checked,onCheckedChange:e=>O(Y,{checked:e},we,pe,V),code:je.code,onCodeChange:e=>O(Y,{code:e},we,pe,V),title:je.title,onTitleChange:e=>O(Y,{title:e},we,pe,V),codeError:qe,error:Se,existingBank:ye[Y]||null})})]})}),e("div",{className:"flex-shrink-0 border-t border-gray-200/80 bg-white/95 shadow-[0_-4px_20px_rgba(0,0,0,0.08)] backdrop-blur-sm",children:t("div",{className:"mx-auto flex max-w-4xl items-center justify-between px-6 py-4",children:[t(p,{variant:"outline",onClick:()=>Me(Y-1),disabled:0===Y,className:"gap-2 border-gray-300 hover:bg-gray-50",children:[e(l,{className:"h-4 w-4"}),"Câu trước"]}),t("div",{className:"flex items-center gap-4",children:[me?t("div",{className:"flex items-center gap-2 rounded-full bg-amber-50 px-3 py-1.5 ring-1 ring-amber-200",children:[e("span",{className:"h-2 w-2 animate-pulse rounded-full bg-amber-500"}),e("span",{className:"text-xs font-medium text-amber-700",children:"Chưa lưu"})]}):ee[Y]?.isCompleted?t("div",{className:"flex items-center gap-2 rounded-full bg-green-50 px-3 py-1.5 ring-1 ring-green-200",children:[e(d,{className:"h-3.5 w-3.5 text-green-500"}),e("span",{className:"text-xs font-medium text-green-700",children:"Đã lưu"})]}):null,t("div",{className:"flex items-center gap-2 rounded-full bg-gray-100 px-4 py-2",children:[t("span",{className:"text-sm font-bold text-gray-900",children:["Câu ",Y+1]}),e("span",{className:"text-sm text-gray-400",children:"/"}),e("span",{className:"text-sm text-gray-500",children:B.totalQuestions})]}),e(p,{onClick:()=>{ze(ie(Y))},disabled:de||!me,className:"gap-2 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 shadow-md hover:shadow-lg transition-all",children:t(n,de?{children:[e(c,{className:"h-4 w-4 animate-spin"}),"Đang lưu..."]}:{children:[e(m,{className:"h-4 w-4"}),"Lưu câu hỏi"]})})]}),t(p,{variant:"outline",onClick:()=>Me(Y+1),disabled:Y>=B.totalQuestions-1||!ee[Y]?.isCompleted,className:"gap-2 border-gray-300 hover:bg-gray-50",children:["Câu tiếp theo",e(u,{className:"h-4 w-4"})]})]})})]}),e("div",{className:"w-80 overflow-y-auto border-l border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(y,{template:B,currentQuestionIndex:Y,completedQuestions:Ue,onQuestionChange:Me})})]}),e(I,{open:be,onOpenChange:ve,completedCount:Ue.size,totalQuestions:B.totalQuestions,onConfirm:()=>{ve(!1),q(V),X(),L.onSave?.(Pe)}}),e(k,{open:he,onOpenChange:xe,onConfirm:()=>{const e=z.current[Y];e?(te(Y,e),E(V,Y+1,S.SAVED)):ne(Y),pe(!1),null!==ge&&$e(ge),fe(null),xe(!1)},onCancel:()=>xe(!1),questionNumber:Y+1})]})}function O(e,t,n,o,i){n(n=>{const o=n[e]??{checked:!1,code:"",title:""};return{...n,[e]:{...o,...t}}}),o(!0),E(i,e+1,S.UNSAVED)}function _(e){return e&&"sectionName"in e?e.sectionName:void 0}
|
|
@@ -5,5 +5,5 @@ interface ExamPartTabsProps {
|
|
|
5
5
|
completedQuestions: Set<number>;
|
|
6
6
|
onPartChange: (partId: string) => void;
|
|
7
7
|
}
|
|
8
|
-
export declare function ExamPartTabs({ template, currentPartId, completedQuestions, onPartChange }: ExamPartTabsProps): import("react").JSX.Element;
|
|
8
|
+
export declare function ExamPartTabs({ template, currentPartId, completedQuestions, onPartChange, }: ExamPartTabsProps): import("react").JSX.Element;
|
|
9
9
|
export {};
|
|
@@ -1,40 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { CheckCircle2, Circle, Layers, Lock, Play } from 'lucide-react';
|
|
4
|
-
import { cn } from '../../shared/lib/utils';
|
|
5
|
-
export function ExamPartTabs({ template, currentPartId, completedQuestions, onPartChange }) {
|
|
6
|
-
const getProgress = (part) => {
|
|
7
|
-
const total = part.endIndex - part.startIndex + 1;
|
|
8
|
-
let completed = 0;
|
|
9
|
-
for (let i = part.startIndex; i <= part.endIndex; i++)
|
|
10
|
-
if (completedQuestions.has(i))
|
|
11
|
-
completed++;
|
|
12
|
-
return { completed, total, percentage: total > 0 ? Math.round((completed / total) * 100) : 0 };
|
|
13
|
-
};
|
|
14
|
-
const getStatus = (part) => {
|
|
15
|
-
const progress = getProgress(part);
|
|
16
|
-
if (progress.completed === 0)
|
|
17
|
-
return 'not-started';
|
|
18
|
-
if (progress.completed === progress.total)
|
|
19
|
-
return 'completed';
|
|
20
|
-
return 'in-progress';
|
|
21
|
-
};
|
|
22
|
-
const isUnlocked = (partIndex) => template.parts.slice(0, partIndex).every((part) => getStatus(part) === 'completed');
|
|
23
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2 px-1", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600 shadow-sm", children: _jsx(Layers, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-bold text-gray-900", children: "C\u00E1c ph\u1EA7n thi" }), _jsxs("p", { className: "text-xs text-gray-500", children: [template.parts.length, " ph\u1EA7n"] })] })] }), _jsx("div", { className: "space-y-3", children: template.parts.map((part, index) => {
|
|
24
|
-
const progress = getProgress(part);
|
|
25
|
-
const status = getStatus(part);
|
|
26
|
-
const active = part.id === currentPartId;
|
|
27
|
-
const unlocked = isUnlocked(index);
|
|
28
|
-
return (_jsx("button", { onClick: () => unlocked && onPartChange(part.id), disabled: !unlocked, className: cn('w-full rounded-xl border-2 p-4 text-left transition-all', active && unlocked ? 'border-transparent bg-blue-600 text-white shadow-lg' : 'border-gray-200 bg-white hover:border-gray-300', !unlocked && 'cursor-not-allowed opacity-60'), children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx(StatusIcon, { status: status, active: active, unlocked: unlocked }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "truncate text-sm font-semibold", children: part.name }), _jsxs("span", { className: "rounded-full bg-black/10 px-2 py-0.5 text-xs font-bold", children: [progress.completed, "/", progress.total] })] }), part.description && _jsx("p", { className: cn('mt-1 text-xs', active ? 'text-blue-100' : 'text-gray-500'), children: part.description }), _jsx("div", { className: cn('mt-2 h-1.5 overflow-hidden rounded-full', active ? 'bg-white/20' : 'bg-gray-200'), children: _jsx("div", { className: cn('h-full rounded-full', active ? 'bg-white' : 'bg-green-500'), style: { width: `${progress.percentage}%` } }) })] })] }) }, part.id));
|
|
29
|
-
}) })] }));
|
|
30
|
-
}
|
|
31
|
-
function StatusIcon({ status, active, unlocked }) {
|
|
32
|
-
const className = cn('h-4 w-4', active ? 'text-white' : 'text-gray-500');
|
|
33
|
-
if (!unlocked)
|
|
34
|
-
return _jsx(Lock, { className: className });
|
|
35
|
-
if (status === 'completed')
|
|
36
|
-
return _jsx(CheckCircle2, { className: className });
|
|
37
|
-
if (status === 'in-progress')
|
|
38
|
-
return _jsx(Play, { className: className });
|
|
39
|
-
return _jsx(Circle, { className: className });
|
|
40
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{CheckCircle2 as r,ChevronRight as a,Circle as s,Layers as n,Lock as l,Play as i}from"lucide-react";import{cn as o}from"../../shared/lib/utils";export function ExamPartTabs({template:d,currentPartId:c,completedQuestions:m,onPartChange:g}){const h=e=>{let t=0;for(let r=e.startIndex;r<=e.endIndex;r++)m.has(r)&&t++;const r=e.endIndex-e.startIndex+1;return{completed:t,total:r,percentage:r>0?Math.round(t/r*100):0}},b=e=>{const t=h(e);return 0===t.completed?"not-started":t.completed===t.total?"completed":"in-progress"},x=e=>{if(0===e)return!0;for(let t=0;t<e;t++){const e=d.parts[t];if("completed"!==b(e))return!1}return!0},p=(t,a,n)=>n?"completed"===t?e(r,{className:o("h-4 w-4",a?"text-white":"text-green-500")}):"in-progress"===t?e(i,{className:o("h-4 w-4",a?"text-white":"text-amber-500")}):e(s,{className:o("h-4 w-4",a?"text-white/70":"text-gray-400")}):e(l,{className:"h-4 w-4 text-gray-400"}),u=(r,s,n,i,d,c)=>t("button",{onClick:()=>c&&g(r.id),disabled:!c,className:o("group relative w-full rounded-xl border-2 p-4 text-left transition-all duration-200","focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none",{"border-transparent bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow-lg shadow-blue-200/50":d&&c,"border-green-200 bg-gradient-to-r from-green-50 to-emerald-50 hover:border-green-300 hover:shadow-md":!d&&"completed"===i&&c,"border-amber-200 bg-gradient-to-r from-amber-50 to-orange-50 hover:border-amber-300 hover:shadow-md":!d&&"in-progress"===i&&c,"border-gray-200 bg-white hover:border-gray-300 hover:shadow-md":!d&&"not-started"===i&&c,"cursor-not-allowed border-gray-100 bg-gray-50/50 opacity-60":!c}),children:[d&&e("div",{className:"absolute -left-[2px] top-1/2 h-8 w-1 -translate-y-1/2 rounded-full bg-white shadow-sm"}),t("div",{className:"flex items-start gap-3",children:[e("div",{className:o("flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg transition-colors",d?"bg-white/20":"completed"===i?"bg-green-100":"in-progress"===i?"bg-amber-100":"bg-gray-100"),children:p(i,d,c)}),t("div",{className:"flex-1 min-w-0",children:[t("div",{className:"flex items-center justify-between gap-2",children:[e("span",{className:o("text-sm font-semibold truncate",d?"text-white":"completed"===i?"text-green-800":"in-progress"===i?"text-amber-800":"text-gray-800"),children:r.name}),t("span",{className:o("flex-shrink-0 rounded-full px-2 py-0.5 text-xs font-bold",d?"bg-white/20 text-white":"completed"===i?"bg-green-100 text-green-700":"in-progress"===i?"bg-amber-100 text-amber-700":"bg-gray-100 text-gray-600"),children:[n.completed,"/",n.total]})]}),r.description&&e("p",{className:o("mt-1 text-xs leading-relaxed",d?"text-blue-100":"text-gray-500"),children:r.description}),t("div",{className:"mt-2",children:[e("div",{className:o("h-1.5 overflow-hidden rounded-full",d?"bg-white/20":"bg-gray-200"),children:e("div",{className:o("h-full rounded-full transition-all duration-500 ease-out",d?"bg-white":"completed"===i?"bg-gradient-to-r from-green-400 to-emerald-500":"in-progress"===i?"bg-gradient-to-r from-amber-400 to-orange-500":"bg-gray-300"),style:{width:`${n.percentage}%`}})}),t("div",{className:o("mt-1 flex items-center justify-between text-xs",d?"text-blue-100":"text-gray-500"),children:[t("span",{children:[r.questionCount," câu hỏi"]}),t("span",{className:"font-medium",children:[n.percentage,"%"]})]})]}),r.isGroup&&r.groups&&r.groups.length>0&&t("div",{className:o("mt-2 rounded-lg p-2 text-xs",d?"bg-white/10":"bg-gray-100/80"),children:[t("div",{className:o("mb-1 font-medium",d?"text-blue-100":"text-gray-600"),children:[r.groups.length," nhóm câu hỏi:"]}),e("div",{className:"space-y-0.5",children:r.groups.map((r,s)=>t("div",{className:o("flex items-center justify-between",d?"text-blue-100":"text-gray-500"),children:[t("span",{className:"flex items-center gap-1",children:[e(a,{className:"h-3 w-3"}),"Nhóm ",r.number]}),t("span",{className:"font-medium",children:[r.questionCount," câu"]})]},s))})]}),r.sections&&r.sections.length>0&&t("div",{className:o("mt-2 rounded-lg p-2 text-xs",d?"bg-white/10":"bg-gray-100/80"),children:[t("div",{className:o("mb-1 font-medium",d?"text-blue-100":"text-gray-600"),children:[r.sections.length," phần nhỏ:"]}),e("div",{className:"space-y-1",children:r.sections.map((r,s)=>{let n=0;for(let e=r.startIndex;e<=r.endIndex;e++)m.has(e)&&n++;const l=r.endIndex-r.startIndex+1;return t("div",{className:o("flex items-center justify-between",d?"text-blue-100":"text-gray-500"),children:[t("span",{className:"flex items-center gap-1 truncate flex-1 mr-2",children:[e(a,{className:"h-3 w-3 flex-shrink-0"}),e("span",{className:"truncate",children:r.name})]}),t("span",{className:"font-medium flex-shrink-0",children:[n,"/",l]})]},s)})})]}),!c&&t("div",{className:"mt-2 flex items-center gap-1 text-xs text-gray-400",children:[e(l,{className:"h-3 w-3"}),e("span",{children:"Hoàn thành phần trước để mở khóa"})]})]})]})]},r.id);return t("div",{className:"space-y-4",children:[t("div",{className:"flex items-center gap-2 px-1",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600 shadow-sm",children:e(n,{className:"h-4 w-4 text-white"})}),t("div",{children:[e("h3",{className:"text-sm font-bold text-gray-900",children:"Các phần thi"}),t("p",{className:"text-xs text-gray-500",children:[d.parts.length," phần"]})]})]}),e("div",{className:"space-y-4",children:(()=>{const r=[];d.parts.forEach((e,t)=>{const a=e.category||"",s=r[r.length-1];s&&s.groupKey===a?s.parts.push({part:e,originalIndex:t}):r.push({groupKey:a,parts:[{part:e,originalIndex:t}]})});const a={LISTENING:"🎧 LISTENING",READING_AND_WRITING:"📝 READING AND WRITING",READING:"📖 READING",WRITING:"✍️ WRITING",SPEAKING:"🗣️ SPEAKING"},s={LISTENING:{bg:"bg-gradient-to-r from-pink-100 via-rose-100 to-pink-50",border:"border-pink-300",text:"text-pink-900",connector:"bg-pink-300"},READING_AND_WRITING:{bg:"bg-gradient-to-r from-teal-100 via-cyan-100 to-teal-50",border:"border-teal-300",text:"text-teal-900",connector:"bg-teal-300"},READING:{bg:"bg-gradient-to-r from-amber-100 via-yellow-100 to-amber-50",border:"border-amber-300",text:"text-amber-900",connector:"bg-amber-300"},WRITING:{bg:"bg-gradient-to-r from-indigo-100 via-blue-100 to-indigo-50",border:"border-indigo-300",text:"text-indigo-900",connector:"bg-indigo-300"},SPEAKING:{bg:"bg-gradient-to-r from-violet-100 via-purple-100 to-violet-50",border:"border-violet-300",text:"text-violet-900",connector:"bg-violet-300"}};return r.length<=1&&!d.parts.some(e=>e.category)?d.parts.map((e,t)=>{const r=h(e),a=b(e),s=c===e.id,n=x(t);return u(e,0,r,a,s,n)}):r.map((r,n)=>{const l=s[r.groupKey]||{bg:"bg-gray-50",border:"border-gray-200",text:"text-gray-800",connector:"bg-gray-300"},i=a[r.groupKey]||r.groupKey;return t("div",{className:"space-y-2",children:[e("div",{className:o("rounded-lg border-2 px-3 py-2 text-center",l.bg,l.border),children:e("span",{className:o("text-xs font-bold tracking-wide uppercase",l.text),children:i})}),e("div",{className:"relative ml-3 pl-5",children:e("div",{className:"space-y-3",children:r.parts.map(({part:a,originalIndex:s},n)=>{const i=h(a),d=b(a),m=c===a.id,g=x(s),p=0===n,N=n===r.parts.length-1;return t("div",{className:"relative",children:[!p&&e("div",{className:o("absolute -left-5 top-0 h-1/2 w-[2px]",l.connector)}),!N&&e("div",{className:o("absolute -left-5 top-1/2 w-[2px]",l.connector),style:{height:"calc(50% + 12px)"}}),p&&e("div",{className:o("absolute -left-5 -top-2 h-[calc(50%+8px)] w-[2px]",l.connector)}),e("div",{className:o("absolute -left-5 top-1/2 -translate-y-1/2 h-[2px] w-5",l.connector)}),u(a,0,i,d,m,g)]},a.id)})})})]},`group-${n}`)})})()}),t("div",{className:"rounded-xl border border-gray-200 bg-gradient-to-br from-gray-50 to-slate-100 p-4 shadow-sm",children:[e("h4",{className:"mb-3 text-xs font-bold uppercase tracking-wide text-gray-500",children:"Tổng quan"}),t("div",{className:"space-y-2",children:[t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2",children:[e("div",{className:"h-2 w-2 rounded-full bg-blue-500"}),e("span",{className:"text-sm text-gray-600",children:"Tổng câu hỏi"})]}),e("span",{className:"text-sm font-bold text-gray-900",children:d.totalQuestions})]}),t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2",children:[e("div",{className:"h-2 w-2 rounded-full bg-green-500"}),e("span",{className:"text-sm text-gray-600",children:"Đã hoàn thành"})]}),e("span",{className:"text-sm font-bold text-green-600",children:m.size})]}),t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2",children:[e("div",{className:"h-2 w-2 rounded-full bg-amber-500"}),e("span",{className:"text-sm text-gray-600",children:"Còn lại"})]}),e("span",{className:"text-sm font-bold text-amber-600",children:d.totalQuestions-m.size})]})]}),t("div",{className:"mt-3 pt-3 border-t border-gray-200",children:[t("div",{className:"flex items-center justify-between mb-1",children:[e("span",{className:"text-xs text-gray-500",children:"Tiến độ tổng"}),t("span",{className:"text-xs font-bold text-blue-600",children:[Math.round(m.size/d.totalQuestions*100),"%"]})]}),e("div",{className:"h-2 overflow-hidden rounded-full bg-gray-200",children:e("div",{className:"h-full rounded-full bg-gradient-to-r from-blue-500 to-indigo-600 transition-all duration-500",style:{width:m.size/d.totalQuestions*100+"%"}})})]})]})]})}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ExamQuestionsSdkApi } from '../../api/exam-questions.types';
|
|
2
|
+
export interface ExamPreviewDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
/** Exam (đề) to preview. `null` while nothing is selected. */
|
|
6
|
+
examId: string | null;
|
|
7
|
+
/** Questions API adapter — provides `getExam` + `getQuestions`. */
|
|
8
|
+
api: ExamQuestionsSdkApi;
|
|
9
|
+
/** Fallback heading shown until the exam name loads. */
|
|
10
|
+
title?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Read-only popup that previews an exam (đề) by id: the layout mirrors the
|
|
14
|
+
* exam taking experience (left sidebar with exam info + progress + question
|
|
15
|
+
* navigation, right pane showing one question at a time) but creates NO attempt
|
|
16
|
+
* — exam + questions are fetched through {@link ExamQuestionsSdkApi}, so it
|
|
17
|
+
* matches exactly how the đề looks. Each question renders via {@link QuestionViewer}
|
|
18
|
+
* in review mode (correct answers highlighted).
|
|
19
|
+
*/
|
|
20
|
+
export declare function ExamPreviewDialog({ open, onOpenChange, examId, api, title }: ExamPreviewDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useEffect as a,useMemo as n,useRef as i,useState as s}from"react";import{ArrowLeft as r,ArrowRight as l,Eye as d,Loader2 as m}from"lucide-react";import{Badge as c}from"../../components/ui/badge";import{Button as o}from"../../components/ui/button";import{Card as p,CardContent as x,CardHeader as u,CardTitle as h}from"../../components/ui/card";import{Dialog as g,DialogContent as f,DialogHeader as N,DialogTitle as v}from"../../components/ui/dialog";import{Progress as b}from"../../components/ui/progress";import{useT as y}from"../../shared/lib/i18n";import{QuestionViewer as w}from"../questions/viewer/QuestionViewer";function j(e){return e&&"object"==typeof e&&"data"in e?e.data:e}function I(e){switch(e?.toUpperCase()){case"EASY":return"bg-green-100 text-green-800";case"MEDIUM":return"bg-orange-100 text-orange-800";case"HARD":return"bg-red-100 text-red-800";default:return"bg-gray-100 text-gray-800"}}export function ExamPreviewDialog({open:q,onOpenChange:S,examId:C,api:Q,title:k}){const E=y(),[P,z]=s(null),[A,M]=s([]),[$,D]=s(0),[T,O]=s("all"),[R,U]=s(!1),[H,V]=s(!1),[Y,B]=s(null),[F,G]=s(0),J=i(null);a(()=>{if(!q||!C)return;let e=!1;return V(!0),B(null),D(0),O("all"),(async()=>{try{const t=j(await Q.getExam(C)),a=Q.getQuestions?j(await Q.getQuestions(C)):[];if(e)return;const n=[...a].sort((e,t)=>(e.questionNumber??0)-(t.questionNumber??0));z(t),M(n)}catch{e||B(E("admin.exams.preview.loadError"))}finally{e||V(!1)}})(),()=>{e=!0}},[q,C,F,Q,E]),a(()=>{q||(z(null),M([]),D(0),O("all"),U(!1),B(null))},[q]);const K=n(()=>{const e=(P?.template?.parts??[]).map(e=>({id:e.id,name:e.name||e.title})),t=new Set(e.map(e=>e.id));for(const a of A){const n=a.partId??"";n&&!t.has(n)&&(t.add(n),e.push({id:n}))}return e},[P,A]),L=e=>{const t=K.findIndex(t=>t.id===(e??""));return t>=0?t+1:1},W=n(()=>{const e=new Map;for(const t of A){const a=t.partId??"";if(!e.has(a)){const t=K.findIndex(e=>e.id===a),n=t>=0?t+1:1;e.set(a,{partId:a,partNo:n,partName:K[t]?.name||`Part ${n}`,questions:[]})}e.get(a).questions.push(t)}return Array.from(e.values()).sort((e,t)=>e.partNo-t.partNo)},[A,K]),X=P?.name??k??"",Z=P?.template?.totalTime??P?.duration??0,_=function(e){return e.reduce((e,t)=>e+(Number(t.points)||0),0)}(A),ee=A[$],te=A.length>0?($+1)/A.length*100:0,ae=e=>{D(e),J.current?.scrollTo({top:0,behavior:"smooth"})},ne="all"===T?A:A.filter(e=>L(e.partId)===T);return e(g,{open:q,onOpenChange:S,children:t(f,{className:"max-h-[95vh] max-w-7xl overflow-y-auto",children:[e(N,{children:t(v,{className:"flex items-center gap-2",children:[e(d,{className:"h-5 w-5 text-blue-600"}),E("admin.exams.preview.title",{name:X})]})}),t("div",{className:"flex h-[80vh] flex-col gap-4 sm:flex-row",children:[e("div",{className:"w-full overflow-y-auto border-b border-gray-200 sm:w-80 sm:border-b-0 sm:border-r",children:t("div",{className:"space-y-4 p-4",children:[t(p,{children:[e(u,{className:"pb-3",children:e(h,{className:"text-base",children:E("admin.exams.preview.examInfo")})}),t(x,{className:"space-y-3",children:[t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-sm text-gray-600",children:E("admin.exams.preview.duration")}),e("span",{className:"text-sm font-medium",children:E("admin.exams.view.minutes",{count:String(Z)})})]}),t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-sm text-gray-600",children:E("admin.exams.preview.questionCount")}),e("span",{className:"text-sm font-medium",children:H?"...":E("admin.exams.preview.questions",{count:String(A.length)})})]}),t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-sm text-gray-600",children:E("admin.exams.preview.totalPoints")}),e("span",{className:"text-sm font-medium",children:E("admin.exams.preview.points",{count:String(_)})})]})]})]}),!H&&A.length>0&&e(p,{children:e(x,{className:"p-4",children:t("div",{className:"space-y-2",children:[t("div",{className:"flex justify-between text-sm",children:[e("span",{children:E("admin.exams.preview.progress")}),t("span",{className:"font-medium",children:[$+1,"/",A.length]})]}),e(b,{value:te,className:"h-2"})]})})}),H?e("div",{className:"flex items-center justify-center py-4",children:t("div",{className:"flex items-center gap-2 text-gray-500",children:[e(m,{className:"h-4 w-4 animate-spin"}),e("span",{className:"text-sm",children:E("admin.exams.preview.loading")})]})}):A.length>0&&t("div",{className:"space-y-3",children:[e("h4",{className:"text-sm font-medium text-gray-700",children:E("admin.exams.preview.questionNavigation")}),t("div",{className:"flex flex-wrap gap-2",children:[e(o,{variant:"all"===T?"default":"outline",size:"sm",onClick:()=>O("all"),className:"h-8 px-3 text-xs font-medium",children:E("admin.exams.preview.all")}),W.map(e=>t(o,{variant:T===e.partNo?"default":"outline",size:"sm",onClick:()=>{O(e.partNo);const t=e.questions[0];if(t){const e=A.findIndex(e=>e.id===t.id);-1!==e&&ae(e)}},className:"h-8 px-3 text-xs font-medium",children:["P",e.partNo]},e.partId||`part-${e.partNo}`))]}),e("div",{className:"rounded bg-gray-50 p-2 text-xs",children:e("span",{className:"font-medium",children:"all"===T?E("admin.exams.preview.allQuestions",{count:String(A.length)}):E("admin.exams.preview.partQuestions",{part:String(T),count:String(W.find(e=>e.partNo===T)?.questions.length??0)})})}),e("div",{className:"rounded-lg bg-gray-50 p-3",children:e("div",{className:"grid max-h-64 grid-cols-6 gap-2 overflow-y-auto pr-2",children:ne.map(t=>{const a=A.findIndex(e=>e.id===t.id);return e(o,{variant:"ghost",size:"sm",onClick:()=>ae(a),className:"h-8 w-full p-0 text-xs font-medium transition-all duration-200 "+(a===$?"border-2 border-blue-500 bg-blue-500 text-white hover:bg-blue-600":"border border-gray-300 bg-white hover:bg-gray-100"),children:t.questionNumber},t.id)})})})]}),!H&&A.length>0&&t("div",{children:[t("div",{className:"mb-2 flex items-center justify-between",children:[e("h4",{className:"text-sm font-medium text-gray-700",children:X}),e(o,{variant:"ghost",size:"sm",onClick:()=>U(e=>!e),className:"h-6 px-2 text-xs text-gray-600 hover:text-gray-900",children:E(R?"admin.exams.preview.collapse":"admin.exams.preview.detail")})]}),e("div",{className:"rounded-lg bg-gray-50 p-3",children:t("div",{className:"flex items-center justify-between",children:[t("div",{children:[t("span",{className:"text-sm font-medium text-gray-900",children:["Total: ",E("admin.exams.preview.questions",{count:String(A.length)})]}),e("div",{className:"mt-1 flex gap-3",children:t("span",{className:"text-xs text-gray-600",children:[W.length," part(s)"]})})]}),e("div",{className:"text-xs text-gray-500",children:E("admin.exams.view.minutes",{count:String(Z)})})]})}),R&&e("div",{className:"mt-3 space-y-2",children:W.map(a=>{const n=L(ee?.partId)===a.partNo;return e("div",{className:"cursor-pointer rounded-lg border-2 p-3 transition-all duration-200 "+(n?"border-blue-400 bg-blue-50 shadow-md":"border-gray-200 bg-white hover:border-gray-300"),onClick:()=>{const e=a.questions[0];if(e){const t=A.findIndex(t=>t.id===e.id);-1!==t&&(ae(t),O(a.partNo))}},children:t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-sm font-medium "+(n?"text-blue-900":"text-gray-900"),children:a.partName}),e(c,{variant:"secondary",className:"text-xs "+(n?"bg-blue-200 text-blue-800":"bg-gray-100 text-gray-700"),children:E("admin.exams.preview.questions",{count:String(a.questions.length)})})]})},a.partId||`part-${a.partNo}`)})})]})]})}),e("div",{className:"flex-1 overflow-y-auto",children:t("div",{className:"flex h-full flex-col",children:[t("div",{className:"flex items-center justify-between border-b pb-4",children:[t("div",{children:[e("h3",{className:"text-lg font-semibold",children:H?e("span",{className:"text-gray-400",children:E("admin.exams.preview.loading")}):A.length>0?E("admin.exams.preview.questionIndex",{index:String($+1),total:String(A.length)}):E("admin.exams.preview.noQuestions")}),ee&&e("div",{className:"mt-1 inline-flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5",children:e("span",{className:"text-sm font-semibold text-blue-700",children:(e=>{const t=K.findIndex(t=>t.id===(e??""));return K[t]?.name||`Part ${t>=0?t+1:1}`})(ee.partId)})})]}),ee&&t("div",{className:"flex items-center gap-2",children:[e(c,{variant:"outline",children:E("admin.exams.preview.points",{count:String(Number(ee.points)||1)})}),e(c,{className:I(ee.difficulty),children:ee.difficulty??"N/A"})]})]}),e("div",{ref:J,className:"flex-1 overflow-y-auto py-4",children:H?t("div",{className:"flex flex-col items-center justify-center space-y-4 py-12",children:[e(m,{className:"h-8 w-8 animate-spin text-blue-500"}),e("p",{className:"text-gray-600",children:E("admin.exams.preview.loadingQuestions")})]}):Y?e("div",{className:"flex flex-col items-center justify-center space-y-4 py-12",children:t("div",{className:"rounded-lg border border-red-200 bg-red-50 p-6 text-center",children:[e("p",{className:"font-medium text-red-800",children:Y}),e(o,{variant:"outline",onClick:()=>G(e=>e+1),className:"mt-4",children:E("admin.exams.preview.retry")})]})}):0===A.length?e("div",{className:"flex flex-col items-center justify-center space-y-4 py-12",children:e("div",{className:"rounded-lg border border-yellow-200 bg-yellow-50 p-6 text-center",children:e("p",{className:"font-medium text-yellow-800",children:E("admin.exams.preview.examNoQuestions")})})}):ee?e(w,{question:ee,isReviewMode:!0}):null}),!H&&A.length>0&&t("div",{className:"mt-auto flex items-center justify-between border-t border-gray-200 bg-gray-50 px-4 py-3",children:[t(o,{variant:"outline",onClick:()=>{$>0&&ae($-1)},disabled:0===$,className:"flex h-9 items-center gap-2 px-3 text-sm",children:[e(r,{className:"h-4 w-4"}),e("span",{className:"hidden sm:inline",children:E("admin.exams.preview.prevQuestion")}),e("span",{className:"sm:hidden",children:E("admin.exams.preview.prev")})]}),t("div",{className:"text-sm text-gray-600",children:[$+1," / ",A.length]}),t(o,{variant:"outline",onClick:()=>{$<A.length-1&&ae($+1)},disabled:$===A.length-1,className:"flex h-9 items-center gap-2 px-3 text-sm",children:[e("span",{className:"hidden sm:inline",children:E("admin.exams.preview.nextQuestion")}),e("span",{className:"sm:hidden",children:E("admin.exams.preview.next")}),e(l,{className:"h-4 w-4"})]})]})]})})]}),e("div",{className:"border-t pt-4",children:t("div",{className:"flex items-center justify-between",children:[e("div",{className:"text-sm text-gray-600",children:E("admin.exams.preview.footerNote")}),e(o,{variant:"outline",onClick:()=>S(!1),className:"border-gray-300",children:E("admin.exams.preview.close")})]})})]})})}
|
|
@@ -5,5 +5,5 @@ interface ExamQuestionGridProps {
|
|
|
5
5
|
completedQuestions: Set<number>;
|
|
6
6
|
onQuestionChange: (questionIndex: number) => void;
|
|
7
7
|
}
|
|
8
|
-
export declare function ExamQuestionGrid({ template, currentQuestionIndex, completedQuestions, onQuestionChange }: ExamQuestionGridProps): import("react").JSX.Element;
|
|
8
|
+
export declare function ExamQuestionGrid({ template, currentQuestionIndex, completedQuestions, onQuestionChange, }: ExamQuestionGridProps): import("react").JSX.Element;
|
|
9
9
|
export {};
|
|
@@ -1,18 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { CheckCircle2, Circle, ListOrdered, Zap } from 'lucide-react';
|
|
4
|
-
import { cn } from '../../shared/lib/utils';
|
|
5
|
-
import { getPartByQuestionIndex, getSectionForQuestionIndex } from './exam-template.utils';
|
|
6
|
-
export function ExamQuestionGrid({ template, currentQuestionIndex, completedQuestions, onQuestionChange }) {
|
|
7
|
-
return (_jsxs("div", { className: "space-y-5", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-9 w-9 items-center justify-center rounded-lg bg-blue-600 shadow-sm", children: _jsx(ListOrdered, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-bold text-gray-900", children: "Danh s\u00E1ch c\u00E2u h\u1ECFi" }), _jsx("p", { className: "text-xs text-gray-500", children: "Click \u0111\u1EC3 chuy\u1EC3n c\u00E2u" })] })] }), _jsx("div", { className: "rounded-xl border border-gray-200 bg-gradient-to-b from-gray-50 to-white p-4 shadow-inner", children: _jsx("div", { className: "grid max-h-[320px] grid-cols-6 justify-center gap-3 overflow-y-auto px-3 py-2", children: Array.from({ length: template.totalQuestions }, (_, index) => {
|
|
8
|
-
const part = getPartByQuestionIndex(template, index);
|
|
9
|
-
const section = part ? getSectionForQuestionIndex(part, index) : undefined;
|
|
10
|
-
return _jsx("button", { onClick: () => onQuestionChange(index), className: buttonClass(index, currentQuestionIndex, completedQuestions), title: [`Câu ${index + 1}`, part?.name, section?.name].filter(Boolean).join(' - '), children: index + 1 }, index);
|
|
11
|
-
}) }) }), _jsxs("div", { className: "rounded-xl border border-gray-100 bg-white p-3 shadow-sm", children: [_jsx("div", { className: "mb-2 text-xs font-bold uppercase tracking-wide text-gray-400", children: "Ch\u00FA th\u00EDch" }), _jsx(Legend, { icon: _jsx(Zap, { className: "h-3.5 w-3.5 text-white" }), label: "C\u00E2u \u0111ang l\u00E0m", className: "bg-blue-600" }), _jsx(Legend, { icon: _jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-white" }), label: "\u0110\u00E3 ho\u00E0n th\u00E0nh", className: "bg-green-500" }), _jsx(Legend, { icon: _jsx(Circle, { className: "h-3.5 w-3.5 text-gray-400" }), label: "Ch\u01B0a t\u1EA1o", className: "border bg-white" })] })] }));
|
|
12
|
-
}
|
|
13
|
-
function buttonClass(index, current, completed) {
|
|
14
|
-
return cn('h-8 w-8 rounded-lg border-2 text-xs font-semibold shadow-sm transition-all hover:scale-105', index === current && 'border-transparent bg-blue-600 text-white ring-2 ring-blue-400', index !== current && completed.has(index) && 'border-transparent bg-green-500 text-white', index !== current && !completed.has(index) && 'border-gray-200 bg-white text-gray-500 hover:bg-gray-50');
|
|
15
|
-
}
|
|
16
|
-
function Legend({ icon, label, className }) {
|
|
17
|
-
return _jsxs("div", { className: "mb-2 flex items-center gap-3", children: [_jsx("div", { className: cn('flex h-7 w-7 items-center justify-center rounded-lg', className), children: icon }), _jsx("span", { className: "text-xs font-medium text-gray-700", children: label })] });
|
|
18
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{CheckCircle2 as r,Circle as a,ListOrdered as s,Zap as n}from"lucide-react";import{cn as i}from"../../shared/lib/utils";import{getPartByQuestionIndex as o,getSectionForQuestionIndex as d}from"./exam-template.utils";export function ExamQuestionGrid({template:l,currentQuestionIndex:c,completedQuestions:m,onQuestionChange:h}){const g=e=>{const t=(e=>e===c?"current":m.has(e)?"completed":"not-started")(e);return i("h-8 w-8 rounded-lg text-xs font-semibold transition-all duration-200","hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-1","border-2 cursor-pointer shadow-sm",{"bg-gradient-to-br from-blue-500 to-indigo-600 text-white border-transparent shadow-lg shadow-blue-300/50 ring-2 ring-blue-400 ring-offset-1":"current"===t,"bg-gradient-to-br from-green-400 to-emerald-500 text-white border-transparent hover:shadow-md hover:shadow-green-200":"completed"===t,"bg-white text-gray-500 border-gray-200 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700":"not-started"===t})},u=Array.from({length:l.totalQuestions},(e,t)=>t);return t("div",{className:"space-y-5",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-9 w-9 items-center justify-center rounded-lg bg-blue-600 shadow-sm",children:e(s,{className:"h-4 w-4 text-white"})}),t("div",{children:[e("h3",{className:"text-sm font-bold text-gray-900",children:"Danh sách câu hỏi"}),e("p",{className:"text-xs text-gray-500",children:"Click để chuyển câu"})]})]}),e("div",{className:"rounded-xl border border-gray-200 bg-gradient-to-b from-gray-50 to-white p-4 shadow-inner",children:e("div",{className:"grid max-h-[320px] grid-cols-6 gap-3 overflow-y-auto px-3 py-2 justify-center",children:u.map(t=>{const r=t+1,a=o(l,t),s=a?d(a,t):void 0,n=[`Câu ${r}`,a?.name||"Unknown Part"];return s&&n.push(s.name),e("button",{onClick:()=>h(t),className:g(t),title:n.join(" - "),children:r},t)})})}),t("div",{className:"rounded-xl border border-gray-100 bg-white p-3 shadow-sm",children:[e("div",{className:"mb-2 text-xs font-bold uppercase tracking-wide text-gray-400",children:"Chú thích"}),t("div",{className:"grid grid-cols-1 gap-2",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 shadow-md",children:e(n,{className:"h-3.5 w-3.5 text-white"})}),e("span",{className:"text-xs font-medium text-gray-700",children:"Câu đang làm"})]}),t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-green-400 to-emerald-500 shadow-sm",children:e(r,{className:"h-3.5 w-3.5 text-white"})}),e("span",{className:"text-xs font-medium text-gray-700",children:"Đã hoàn thành"})]}),t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-7 w-7 items-center justify-center rounded-lg border-2 border-gray-200 bg-white",children:e(a,{className:"h-3.5 w-3.5 text-gray-400"})}),e("span",{className:"text-xs font-medium text-gray-700",children:"Chưa tạo"})]})]})]})]})}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { UserRole } from '../../shared/types/common.types';
|
|
2
2
|
import type { ExamQuestionsSdkApi } from '../../api/exam-questions.types';
|
|
3
|
+
import type { CreateExamTexts } from './create-exam.texts';
|
|
3
4
|
export interface ExamQuestionsPageContainerProps {
|
|
4
5
|
examId: string;
|
|
5
6
|
role?: UserRole.ADMIN | UserRole.TEACHER;
|
|
6
7
|
api: ExamQuestionsSdkApi;
|
|
7
8
|
listPath?: string;
|
|
8
9
|
onNavigate?: (href: string) => void;
|
|
10
|
+
/** Optional label overrides for embedded contexts. */
|
|
11
|
+
texts?: CreateExamTexts;
|
|
9
12
|
}
|
|
10
13
|
export declare function ExamQuestionsPageContainer(props: ExamQuestionsPageContainerProps): import("react").JSX.Element;
|