@tinyweb_dev/oe-exam-sdk 0.1.3 → 0.1.5
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/README.md +112 -13
- package/dist/api/contest-rooms.d.ts +13 -0
- package/dist/api/contest-rooms.js +1 -0
- package/dist/api/contest-rooms.types.d.ts +47 -0
- package/dist/api/contest-rooms.types.js +1 -0
- package/dist/api/exam-entry.d.ts +14 -0
- package/dist/api/exam-entry.js +1 -0
- package/dist/api/exam-entry.types.d.ts +93 -0
- package/dist/api/exam-entry.types.js +1 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +1 -1
- package/dist/api/result-review.d.ts +2 -1
- package/dist/api/result-review.js +1 -1
- package/dist/api/result-review.types.d.ts +69 -3
- package/dist/api/result-review.types.js +1 -1
- package/dist/components/exams/entry/EntryRestartDialog.d.ts +12 -0
- package/dist/components/exams/entry/EntryRestartDialog.js +1 -0
- package/dist/components/exams/entry/StudentExamEntryPageContainer.d.ts +31 -0
- package/dist/components/exams/entry/StudentExamEntryPageContainer.js +1 -0
- package/dist/components/exams/entry/exam-entry.states.d.ts +22 -0
- package/dist/components/exams/entry/exam-entry.states.js +1 -0
- package/dist/components/exams/entry/exam-entry.utils.d.ts +16 -0
- package/dist/components/exams/entry/exam-entry.utils.js +1 -0
- package/dist/components/exams/entry/fullscreen-loading.d.ts +5 -0
- package/dist/components/exams/entry/fullscreen-loading.js +1 -0
- package/dist/components/exams/entry/index.d.ts +7 -0
- package/dist/components/exams/entry/index.js +1 -0
- package/dist/components/exams/index.d.ts +8 -0
- package/dist/components/exams/index.js +1 -1
- package/dist/components/exams/rooms/StudentContestRoomsPageContainer.d.ts +18 -0
- package/dist/components/exams/rooms/StudentContestRoomsPageContainer.js +1 -0
- package/dist/components/exams/rooms/contest-rooms.utils.d.ts +32 -0
- package/dist/components/exams/rooms/contest-rooms.utils.js +1 -0
- package/dist/components/exams/rooms/index.d.ts +3 -0
- package/dist/components/exams/rooms/index.js +1 -0
- package/dist/components/exams/take/ExamTakingApiContext.d.ts +10 -0
- package/dist/components/exams/take/ExamTakingApiContext.js +1 -0
- package/dist/components/exams/take/ExamTakingPageContainer.d.ts +28 -4
- package/dist/components/exams/take/ExamTakingPageContainer.js +1 -1
- package/dist/components/exams/take/components/QuestionRenderer.d.ts +25 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +1 -0
- package/dist/components/exams/take/components/index.d.ts +10 -0
- package/dist/components/exams/take/components/index.js +1 -0
- package/dist/components/exams/take/components/question-renderers/ArticlesViewer.d.ts +7 -0
- package/dist/components/exams/take/components/question-renderers/ArticlesViewer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.d.ts +22 -0
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.d.ts +14 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.d.ts +3 -0
- package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +21 -0
- package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.d.ts +19 -0
- package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.d.ts +20 -0
- package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.d.ts +32 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.d.ts +19 -0
- package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.d.ts +22 -0
- package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +1 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +19 -0
- package/dist/components/exams/take/components/question-renderers/index.js +1 -0
- package/dist/components/exams/take/components/speaking/FindObjectInPicture.module.css +133 -0
- package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.d.ts +9 -0
- package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingCompareImages.module.css +153 -0
- package/dist/components/exams/take/components/speaking/SpeakingContentFrame.d.ts +7 -0
- package/dist/components/exams/take/components/speaking/SpeakingContentFrame.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingDragObjects.module.css +238 -0
- package/dist/components/exams/take/components/speaking/SpeakingExamLayout.d.ts +27 -0
- package/dist/components/exams/take/components/speaking/SpeakingExamLayout.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingHeader.d.ts +6 -0
- package/dist/components/exams/take/components/speaking/SpeakingHeader.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingInfoExchange.module.css +193 -0
- package/dist/components/exams/take/components/speaking/SpeakingInfoExchangeSummerSky.module.css +238 -0
- package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.d.ts +5 -0
- package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingMicButton.d.ts +14 -0
- package/dist/components/exams/take/components/speaking/SpeakingMicButton.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingNavButton.d.ts +7 -0
- package/dist/components/exams/take/components/speaking/SpeakingNavButton.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingPartBadge.d.ts +6 -0
- package/dist/components/exams/take/components/speaking/SpeakingPartBadge.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingReadDisplayedContent.module.css +115 -0
- package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.d.ts +11 -0
- package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.d.ts +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.js +1 -0
- package/dist/components/exams/take/components/speaking/SpeakingStoryImages.module.css +231 -0
- package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.d.ts +13 -0
- package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.js +1 -0
- package/dist/components/exams/take/components/speaking/index.d.ts +1 -0
- package/dist/components/exams/take/components/speaking/index.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.d.ts +18 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.d.ts +26 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.d.ts +32 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.d.ts +17 -0
- package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.js +1 -0
- package/dist/components/exams/take/components/speaking/renderers/index.d.ts +13 -0
- package/dist/components/exams/take/components/speaking/renderers/index.js +1 -0
- package/dist/components/exams/take/hooks/index.d.ts +2 -0
- package/dist/components/exams/take/hooks/index.js +1 -0
- package/dist/components/exams/take/hooks/useAnswerAutosave.d.ts +27 -0
- package/dist/components/exams/take/hooks/useAnswerAutosave.js +1 -0
- package/dist/components/exams/take/hooks/useAttemptSubmit.d.ts +29 -0
- package/dist/components/exams/take/hooks/useAttemptSubmit.js +1 -0
- package/dist/components/exams/take/hooks/useAutoSave.d.ts +20 -0
- package/dist/components/exams/take/hooks/useAutoSave.js +1 -0
- package/dist/components/exams/take/hooks/useExamCountdown.d.ts +22 -0
- package/dist/components/exams/take/hooks/useExamCountdown.js +1 -0
- package/dist/components/exams/take/hooks/useExamTimer.d.ts +21 -0
- package/dist/components/exams/take/hooks/useExamTimer.js +1 -0
- package/dist/components/exams/take/hooks/useProctoringCamera.d.ts +19 -0
- package/dist/components/exams/take/hooks/useProctoringCamera.js +1 -0
- package/dist/components/exams/take/hooks/useProctoringSnapshot.d.ts +25 -0
- package/dist/components/exams/take/hooks/useProctoringSnapshot.js +1 -0
- package/dist/components/exams/take/hooks/useSpeakingConversationAudio.d.ts +19 -0
- package/dist/components/exams/take/hooks/useSpeakingConversationAudio.js +1 -0
- package/dist/components/exams/take/hooks/useSpeakingNavigation.d.ts +37 -0
- package/dist/components/exams/take/hooks/useSpeakingNavigation.js +1 -0
- package/dist/components/exams/take/hooks/useSpeakingRecorder.d.ts +45 -5
- package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -1
- package/dist/components/exams/take/hooks/useSpeakingRendererSetup.d.ts +39 -0
- package/dist/components/exams/take/hooks/useSpeakingRendererSetup.js +1 -0
- package/dist/components/exams/take/hooks/useToeicAudio.d.ts +17 -0
- package/dist/components/exams/take/hooks/useToeicAudio.js +1 -0
- package/dist/components/exams/take/index.d.ts +9 -1
- package/dist/components/exams/take/index.js +1 -1
- package/dist/components/exams/take/types/speaking-cue-card.types.d.ts +10 -0
- package/dist/components/exams/take/types/speaking-cue-card.types.js +1 -0
- package/dist/components/exams/take/types/speaking.types.d.ts +153 -0
- package/dist/components/exams/take/types/speaking.types.js +1 -0
- package/dist/components/exams/take/types.d.ts +286 -1
- package/dist/components/exams/take/utils/answer-transformers.d.ts +26 -0
- package/dist/components/exams/take/utils/answer-transformers.js +1 -0
- package/dist/components/exams/take/utils/coloring-assignments.d.ts +5 -0
- package/dist/components/exams/take/utils/coloring-assignments.js +1 -0
- package/dist/components/exams/take/utils/question-transformers.d.ts +362 -0
- package/dist/components/exams/take/utils/question-transformers.js +1 -0
- package/dist/components/features/exam-entry/entry-state-cards.d.ts +19 -0
- package/dist/components/features/exam-entry/entry-state-cards.js +1 -0
- package/dist/components/features/exam-entry/index.d.ts +2 -0
- package/dist/components/features/exam-entry/index.js +1 -0
- package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.d.ts +12 -0
- package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.js +1 -0
- package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.d.ts +14 -0
- package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.js +1 -0
- package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.d.ts +16 -0
- package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.js +1 -0
- package/dist/components/features/exam-entry/themed/components/CloseButton.d.ts +9 -0
- package/dist/components/features/exam-entry/themed/components/CloseButton.js +1 -0
- package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.d.ts +13 -0
- package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.js +1 -0
- package/dist/components/features/exam-entry/themed/components/ModeSelector.d.ts +7 -0
- package/dist/components/features/exam-entry/themed/components/ModeSelector.js +1 -0
- package/dist/components/features/exam-entry/themed/components/StartButton.d.ts +11 -0
- package/dist/components/features/exam-entry/themed/components/StartButton.js +1 -0
- package/dist/components/features/exam-entry/themed/components/index.d.ts +4 -0
- package/dist/components/features/exam-entry/themed/components/index.js +1 -0
- package/dist/components/features/exam-entry/themed/index.d.ts +1 -0
- package/dist/components/features/exam-entry/themed/index.js +1 -0
- package/dist/components/results/ResultReviewPageContainer.d.ts +16 -2
- package/dist/components/results/ResultReviewPageContainer.js +1 -1
- package/dist/components/results/ReviewQuestionRenderer.d.ts +11 -1
- package/dist/components/results/ReviewQuestionRenderer.js +1 -1
- package/dist/components/results/hooks/useResultReviewPolling.d.ts +37 -0
- package/dist/components/results/hooks/useResultReviewPolling.js +1 -0
- package/dist/components/results/index.d.ts +36 -1
- package/dist/components/results/index.js +1 -1
- package/dist/components/results/renderers/QuestionExplanations.d.ts +22 -0
- package/dist/components/results/renderers/QuestionExplanations.js +1 -0
- package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.d.ts +13 -0
- package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.d.ts +22 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewColoringRenderer.d.ts +16 -0
- package/dist/components/results/renderers/ReviewColoringRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +18 -0
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.d.ts +25 -0
- package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.d.ts +25 -0
- package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.d.ts +25 -0
- package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListeningHeader.d.ts +44 -0
- package/dist/components/results/renderers/ReviewListeningHeader.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingAIPanel.d.ts +17 -0
- package/dist/components/results/renderers/ReviewSpeakingAIPanel.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingMedia.d.ts +39 -0
- package/dist/components/results/renderers/ReviewSpeakingMedia.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingQuestionItem.d.ts +5 -0
- package/dist/components/results/renderers/ReviewSpeakingQuestionItem.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingRenderer.d.ts +3 -0
- package/dist/components/results/renderers/ReviewSpeakingRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingRenderer.types.d.ts +80 -0
- package/dist/components/results/renderers/ReviewSpeakingRenderer.types.js +1 -0
- package/dist/components/results/renderers/review-question-body-dedicated.d.ts +15 -0
- package/dist/components/results/renderers/review-question-body-dedicated.js +1 -0
- package/dist/components/results/renderers/review-question-body-movers.d.ts +17 -0
- package/dist/components/results/renderers/review-question-body-movers.js +1 -0
- package/dist/components/results/renderers/review-speaking-map.d.ts +7 -0
- package/dist/components/results/renderers/review-speaking-map.js +1 -0
- package/dist/components/results/report/BadgeTitle.d.ts +5 -0
- package/dist/components/results/report/BadgeTitle.js +1 -0
- package/dist/components/results/report/ExamSkillRow.d.ts +14 -0
- package/dist/components/results/report/ExamSkillRow.js +1 -0
- package/dist/components/results/report/OceanEduExamReport.d.ts +31 -0
- package/dist/components/results/report/OceanEduExamReport.js +1 -0
- package/dist/components/results/report/OceanEduSpeakingReport.d.ts +26 -0
- package/dist/components/results/report/OceanEduSpeakingReport.js +1 -0
- package/dist/components/results/report/OverallAssessmentSection.d.ts +24 -0
- package/dist/components/results/report/OverallAssessmentSection.js +1 -0
- package/dist/components/results/report/ResultErrorState.d.ts +14 -0
- package/dist/components/results/report/ResultErrorState.js +1 -0
- package/dist/components/results/report/ResultLoadingState.d.ts +11 -0
- package/dist/components/results/report/ResultLoadingState.js +1 -0
- package/dist/components/results/report/ResultNotPublicState.d.ts +16 -0
- package/dist/components/results/report/ResultNotPublicState.js +1 -0
- package/dist/components/results/report/SpeakingReportParts.d.ts +4 -0
- package/dist/components/results/report/SpeakingReportParts.js +1 -0
- package/dist/components/results/report/SpeakingReportShared.d.ts +36 -0
- package/dist/components/results/report/SpeakingReportShared.js +1 -0
- package/dist/components/results/report/SpeakingRubricSection.d.ts +21 -0
- package/dist/components/results/report/SpeakingRubricSection.js +1 -0
- package/dist/components/results/report/assets/dofu2.png +0 -0
- package/dist/components/results/report/assets/dofu4.png +0 -0
- package/dist/components/results/report/assets/logo-ocean.png +0 -0
- package/dist/components/results/report/assets/penguin-main.png +0 -0
- package/dist/components/results/report/assets/penguin-small.png +0 -0
- package/dist/components/results/report/speaking-report.utils.d.ts +37 -0
- package/dist/components/results/report/speaking-report.utils.js +1 -0
- package/dist/components/results/review-data.d.ts +1 -0
- package/dist/components/results/review-data.js +1 -1
- package/dist/components/results/review-status.utils.d.ts +12 -0
- package/dist/components/results/review-status.utils.js +1 -0
- package/dist/components/results/review-types.d.ts +14 -0
- package/dist/components/results/speaking/SpeakingResultAudioPlayer.d.ts +17 -0
- package/dist/components/results/speaking/SpeakingResultAudioPlayer.js +1 -0
- package/dist/components/results/speaking/SpeakingReviewResult.d.ts +30 -0
- package/dist/components/results/speaking/SpeakingReviewResult.js +1 -0
- package/dist/components/results/utils/review-score.utils.d.ts +21 -0
- package/dist/components/results/utils/review-score.utils.js +1 -0
- package/dist/components/results/utils/speaking-summary.d.ts +41 -0
- package/dist/components/results/utils/speaking-summary.js +1 -0
- package/dist/components/results/utils/transform-speaking-review.d.ts +62 -0
- package/dist/components/results/utils/transform-speaking-review.js +1 -0
- package/dist/components/shared/audio/SpeakingAudioPlayer.d.ts +12 -0
- package/dist/components/shared/audio/SpeakingAudioPlayer.js +1 -0
- package/dist/components/shared/audio/SpeakingAudioPlayer.module.css +115 -0
- package/dist/components/shared/audio/assets/fr-image-part1-border.png +0 -0
- package/dist/components/shared/audio/assets/ic-speaker.webp +0 -0
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.d.ts +11 -3
- package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -1
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.d.ts +5 -1
- package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -1
- package/dist/components/themes/index.d.ts +11 -0
- package/dist/components/themes/index.js +1 -1
- package/dist/components/themes/spaceship/SpaceshipAudioPlayer.d.ts +8 -0
- package/dist/components/themes/spaceship/SpaceshipAudioPlayer.js +1 -0
- package/dist/components/themes/spaceship/SpaceshipLayout.d.ts +2 -0
- package/dist/components/themes/spaceship/SpaceshipLayout.js +1 -0
- package/dist/components/themes/spaceship/assets/images/X-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/X.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/audio-bar.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/background.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/bg_sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/big-blue-badge-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/big-blue-badge.png +0 -0
- package/dist/components/themes/spaceship/assets/images/big-blue-badge.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/bingo.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/blue-badge-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/blue-badge.png +0 -0
- package/dist/components/themes/spaceship/assets/images/blue-wrapper-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/blue-wrapper.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/board_red.png +0 -0
- package/dist/components/themes/spaceship/assets/images/button_feedback_vi.png +0 -0
- package/dist/components/themes/spaceship/assets/images/circle-btn-right-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/circle-btn-right.png +0 -0
- package/dist/components/themes/spaceship/assets/images/circle-btn-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/circle-btn.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/focus-vector.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/head-phone_red.png +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-mute-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-mute.png +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-play-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-play-violet.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/ic-play.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/micro.png +0 -0
- package/dist/components/themes/spaceship/assets/images/next-small.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/next-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/next.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/purple-circle.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/question-badge-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/question-badge.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/red-badge-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/red-badge.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/red-wrapper-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/red-wrapper.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/robot-avatar.png +0 -0
- package/dist/components/themes/spaceship/assets/images/sound_red.png +0 -0
- package/dist/components/themes/spaceship/assets/images/student-avatar.png +0 -0
- package/dist/components/themes/spaceship/assets/images/teacher-play-sunset.png +0 -0
- package/dist/components/themes/spaceship/assets/images/teacher-play.webp +0 -0
- package/dist/components/themes/spaceship/assets/images/timer-badge.png +0 -0
- package/dist/components/themes/spaceship/assets/styles/SpaceshipLayout.module.css +347 -0
- package/dist/components/themes/spaceship/assets/styles/SpaceshipWaitingRoom.module.css +395 -0
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/shared/lib/stores/examTakingStore.d.ts +54 -0
- package/dist/shared/lib/stores/examTakingStore.js +1 -0
- package/dist/shared/lib/stores/gradingTransientStore.d.ts +23 -0
- package/dist/shared/lib/stores/gradingTransientStore.js +1 -0
- package/dist/shared/lib/themes/config/starters.config.d.ts +3 -0
- package/dist/shared/lib/themes/config/starters.config.js +1 -0
- package/dist/shared/lib/themes/index.d.ts +18 -0
- package/dist/shared/lib/themes/index.js +1 -0
- package/dist/shared/lib/themes/types.d.ts +39 -0
- package/dist/shared/lib/themes/types.js +1 -0
- package/dist/shared/lib/toast.d.ts +19 -0
- package/dist/shared/lib/toast.js +1 -0
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -18,9 +18,11 @@ import {
|
|
|
18
18
|
ExamQuestionsPageContainer,
|
|
19
19
|
ExamTakingPageContainer,
|
|
20
20
|
ResultReviewPageContainer,
|
|
21
|
+
StudentExamEntryPageContainer,
|
|
21
22
|
} from '@tinyweb_dev/oe-exam-sdk';
|
|
22
23
|
import {
|
|
23
24
|
createExamCreateApi,
|
|
25
|
+
createExamEntryApi,
|
|
24
26
|
createExamQuestionsApi,
|
|
25
27
|
createExamTakingApi,
|
|
26
28
|
createResultReviewApi,
|
|
@@ -47,12 +49,26 @@ export function ExamQuestionsPage({ examId, router }) {
|
|
|
47
49
|
);
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
export function
|
|
52
|
+
export function ExamEnterPage({ roomId, inviteToken, router }) {
|
|
53
|
+
return (
|
|
54
|
+
<StudentExamEntryPageContainer
|
|
55
|
+
roomId={roomId}
|
|
56
|
+
inviteToken={inviteToken}
|
|
57
|
+
api={createExamEntryApi({ baseUrl })}
|
|
58
|
+
onNavigate={(href) => router.push(href)}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ExamTakePage({ roomId, router }) {
|
|
51
64
|
return (
|
|
52
65
|
<ExamTakingPageContainer
|
|
53
|
-
|
|
66
|
+
roomId={roomId}
|
|
54
67
|
api={createExamTakingApi({ baseUrl })}
|
|
55
68
|
onNavigate={(href) => router.replace(href)}
|
|
69
|
+
resultPath={(attemptId) => `/student/results/${attemptId}/review`}
|
|
70
|
+
onSubmitted={({ attemptId }) => router.replace(`/student/results/${attemptId}/review`)}
|
|
71
|
+
proctoringEnabled
|
|
56
72
|
/>
|
|
57
73
|
);
|
|
58
74
|
}
|
|
@@ -77,8 +93,9 @@ export function ResultReviewPage({ attemptId, token, router }) {
|
|
|
77
93
|
@tinyweb_dev/oe-exam-sdk/shared # shared constants/types/helpers
|
|
78
94
|
@tinyweb_dev/oe-exam-sdk/mcp # MCP server entry và tools
|
|
79
95
|
@tinyweb_dev/oe-exam-sdk/components # shared UI components
|
|
80
|
-
@tinyweb_dev/oe-exam-sdk/components/exams # màn tạo đề / câu hỏi / làm bài
|
|
81
|
-
@tinyweb_dev/oe-exam-sdk/components/exams/
|
|
96
|
+
@tinyweb_dev/oe-exam-sdk/components/exams # màn tạo đề / câu hỏi / làm bài / vào phòng
|
|
97
|
+
@tinyweb_dev/oe-exam-sdk/components/exams/entry # màn vào phòng (entry) + state machine
|
|
98
|
+
@tinyweb_dev/oe-exam-sdk/components/exams/take # màn làm bài + renderer generic + hooks
|
|
82
99
|
@tinyweb_dev/oe-exam-sdk/components/questions # hệ thống tạo câu hỏi
|
|
83
100
|
@tinyweb_dev/oe-exam-sdk/components/themes # cambridge-yle, english-certification, summer-sky
|
|
84
101
|
@tinyweb_dev/oe-exam-sdk/components/results # màn xem chi tiết kết quả
|
|
@@ -109,6 +126,40 @@ Các adapter mặc định nhận:
|
|
|
109
126
|
|
|
110
127
|
`createExamTakingApi` đã bao gồm các API cần cho màn làm bài: start/resume attempt, questions, save answers, submit, upload audio, proctoring snapshot, realtime grading, grading summary/status/detail, room/attempt detail và `keepalive` save.
|
|
111
128
|
|
|
129
|
+
`createExamEntryApi` phục vụ màn vào phòng: room detail, active attempt theo room, start/delete attempt, join qua invite token và đăng ký contest (registration form).
|
|
130
|
+
|
|
131
|
+
## Màn vào phòng (entry)
|
|
132
|
+
|
|
133
|
+
`StudentExamEntryPageContainer` port toàn bộ luồng `/student/exams/:roomId/enter`: join qua invite, gating quyền truy cập / đề thi / form đăng ký, state machine theo cửa sổ thời gian (BEFORE_START / WITHIN_WINDOW / EXPIRED / ALREADY_SUBMITTED), resume attempt đang dở và dialog "làm tiếp / làm lại từ đầu".
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
<StudentExamEntryPageContainer
|
|
137
|
+
roomId={roomId}
|
|
138
|
+
inviteToken={inviteToken}
|
|
139
|
+
api={createExamEntryApi({ baseUrl })}
|
|
140
|
+
onNavigate={(href) => router.push(href)}
|
|
141
|
+
takePath={(id) => `/student/exams/${id}/take`}
|
|
142
|
+
renderRegistrationForm={({ room, onSubmit, isSubmitting }) => (
|
|
143
|
+
<MyRegistrationDialog room={room} onSubmit={onSubmit} isSubmitting={isSubmitting} />
|
|
144
|
+
)}
|
|
145
|
+
/>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Themed waiting room (TOEIC / Summer Sky / Cambridge YLE) và registration form là phần phụ thuộc host app — truyền qua `renderRegistrationForm` hoặc bọc container.
|
|
149
|
+
|
|
150
|
+
## Hooks màn làm bài
|
|
151
|
+
|
|
152
|
+
`@tinyweb_dev/oe-exam-sdk/components/exams/take` export các hook tái sử dụng:
|
|
153
|
+
|
|
154
|
+
```txt
|
|
155
|
+
useExamCountdown # đếm ngược theo startedAt + duration, auto-submit khi hết giờ
|
|
156
|
+
useAnswerAutosave # lưu debounce theo batch + flush keepalive khi ẩn tab/unload + retry khi online
|
|
157
|
+
useProctoringSnapshot # chụp webcam định kỳ và upload snapshot
|
|
158
|
+
useAttemptSubmit # nộp bài (flush trước), chống double-submit, trả pending/auto-graded answer ids
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`ExamTakingPageContainer` đã nối sẵn các hook trên. `onSubmitted` nhận `{ attemptId, pendingAnswerIds }` để điều hướng sang màn review đúng ngữ cảnh; bật proctoring qua prop `proctoringEnabled` và truyền `achieversExamTitle` nếu cần gate video intro của giáo viên.
|
|
162
|
+
|
|
112
163
|
## Theme làm bài
|
|
113
164
|
|
|
114
165
|
`ExamTakingPageContainer` tự chọn layout theo `examTemplate.theme`:
|
|
@@ -124,18 +175,66 @@ Renderer câu hỏi trong màn làm bài hiện là `GenericQuestionRenderer`.
|
|
|
124
175
|
|
|
125
176
|
## Màn xem kết quả
|
|
126
177
|
|
|
178
|
+
`ResultReviewPageContainer` sở hữu toàn bộ màn review và tự chọn giao diện theo template:
|
|
179
|
+
|
|
180
|
+
- `SPEAKING`: dùng report nói chuyên biệt, chi tiết từng phần/câu, audio, AI assessment và speaking rubric.
|
|
181
|
+
- Template còn lại: dùng `OceanEduExamReport`, phân tích tổng quan, rubric (nếu có) và `ReviewQuestionRenderer` đầy đủ cho câu hỏi dedicated lẫn Movers fallback.
|
|
182
|
+
- Template `PLACEMENT_TEST_KINDY_VER3` và `PLACEMENT_TEST_PRESCHOOL` dùng điểm rubric theo Part và ẩn chấm điểm từng câu.
|
|
183
|
+
|
|
127
184
|
```tsx
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
185
|
+
'use client';
|
|
186
|
+
|
|
187
|
+
import {
|
|
188
|
+
ResultReviewPageContainer,
|
|
189
|
+
createResultReviewApi,
|
|
190
|
+
} from '@tinyweb_dev/oe-exam-sdk';
|
|
191
|
+
|
|
192
|
+
export function ResultReviewPage({ attemptId, token, isPrintMode, router }) {
|
|
193
|
+
const reviewUrl = typeof window !== 'undefined' ? window.location.href : undefined;
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<ResultReviewPageContainer
|
|
197
|
+
attemptId={attemptId}
|
|
198
|
+
token={token}
|
|
199
|
+
api={createResultReviewApi({ baseUrl })}
|
|
200
|
+
isPrintMode={isPrintMode}
|
|
201
|
+
canExportPdf
|
|
202
|
+
reviewUrl={reviewUrl}
|
|
203
|
+
assetBaseUrl="/speaking/ocean-edu"
|
|
204
|
+
loginHref={`/auth/login?redirect=${encodeURIComponent(reviewUrl ?? '')}`}
|
|
205
|
+
onBack={() => router.back()}
|
|
206
|
+
onNavigate={(href) => router.push(href)}
|
|
207
|
+
topActionsSlot={<SummaryVideoAction attemptId={attemptId} />}
|
|
208
|
+
beforeReportSlot={<GiftClaimDialog attemptId={attemptId} />}
|
|
209
|
+
afterReportSlot={<ResultExtraContent attemptId={attemptId} />}
|
|
210
|
+
/>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
136
213
|
```
|
|
137
214
|
|
|
138
|
-
|
|
215
|
+
### Props và hành vi
|
|
216
|
+
|
|
217
|
+
| Prop | Hành vi |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `attemptId`, `token`, `api` | Tải detail/attempt; `token` hỗ trợ link kết quả công khai. |
|
|
220
|
+
| `isPrintMode` | Dừng polling và phát `body[data-pdf-ready="true"]` sau khi load để headless PDF renderer tiếp tục. |
|
|
221
|
+
| `canExportPdf` | Hiện nút xuất PDF khi adapter có `exportPdf`; SDK tải blob bằng tên file từ API. |
|
|
222
|
+
| `reviewUrl` | URL canonical đưa vào QR; SDK bổ sung verification token từ prop hoặc attempt. Mặc định dùng URL hiện tại trên browser. |
|
|
223
|
+
| `assetBaseUrl` | Prefix tùy chọn cho `logo-ocean.png`, `penguin-main.png`, `penguin-small.png`, `dofu2.png`; bỏ prop để dùng asset bundle trong package. |
|
|
224
|
+
| `loginHref` | Đích đăng nhập ở trạng thái 403/private; mặc định là `/auth/login?redirect=<current-url>`. |
|
|
225
|
+
| `onBack`, `onNavigate` | Tích hợp router của host. Nếu có contest, back qua `onNavigate` về rooms của contest; nếu không có callback thì dùng browser history. |
|
|
226
|
+
|
|
227
|
+
Container poll mỗi 5 giây khi còn grading status đang xử lý hoặc LLM assessment chưa sẵn sàng. Các trạng thái loading, lỗi chung và 403 chưa công khai có UI riêng. `showDetailedResults=false` chỉ ẩn phần câu hỏi chi tiết, không ẩn report tổng quan.
|
|
228
|
+
|
|
229
|
+
### Slots và phạm vi host
|
|
230
|
+
|
|
231
|
+
- `topActionsSlot`: action bổ sung cạnh nút xuất PDF, ví dụ summary video.
|
|
232
|
+
- `beforeReportSlot`: nội dung trước report, ví dụ gift claim hoặc thông báo nâng cấp.
|
|
233
|
+
- `afterReportSlot`: nội dung sau report.
|
|
234
|
+
|
|
235
|
+
Gift claim, summary video/remotion, account upgrade, auth store và router không được nhúng trong SDK. Host truyền các phần này qua slot/callback; không cần nối lại page của `oe-exam-fe` để dùng container.
|
|
236
|
+
|
|
237
|
+
Các report shell, renderer, transform helper, polling/status helper và result-review types cũng được export từ `@tinyweb_dev/oe-exam-sdk/components/results` khi host cần tái sử dụng riêng lẻ.
|
|
139
238
|
|
|
140
239
|
## MCP server
|
|
141
240
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ContestRoomListItem, ContestRoomsContestDetail, ContestRoomsFetchApiOptions, ContestRoomsSdkApi } from './contest-rooms.types';
|
|
2
|
+
/**
|
|
3
|
+
* Default fetch-based adapter for the student contest rooms-list flow. Mirrors
|
|
4
|
+
* the host app endpoints used by the original `/student/contests/:contestId/rooms`
|
|
5
|
+
* page: contest detail, room list and room registration.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createContestRoomsApi(options?: ContestRoomsFetchApiOptions): ContestRoomsSdkApi;
|
|
8
|
+
/** In-memory mock adapter for demo/storybook/tests. */
|
|
9
|
+
export declare function createMockContestRoomsApi(seed: {
|
|
10
|
+
contest: ContestRoomsContestDetail;
|
|
11
|
+
rooms: ContestRoomListItem[];
|
|
12
|
+
}): ContestRoomsSdkApi;
|
|
13
|
+
export type { ContestRoomListItem, ContestRoomRegistrationStatus, ContestRoomVisibility, ContestRoomsContestDetail, ContestRoomsFetchApiOptions, ContestRoomsSdkApi, } from './contest-rooms.types';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{API_BASE_URL as t,API_ENDPOINTS as e}from"../shared/constants/ApiConstant";export function createContestRoomsApi(n={}){const o=function({baseUrl:e=t??"",fetcher:n=fetch,headers:o}){return async function(t,s){const r=await n(`${e}${t}`,{credentials:"include",...s,headers:{"Content-Type":"application/json",...o,...s?.headers}});if(!r.ok)throw new Error(await async function(t){const e=await t.json().catch(()=>{});if("object"==typeof e&&null!==e&&"message"in e&&"string"==typeof e.message)return e.message;return t.statusText}(r));if(204!==r.status)return r.json()}}(n);return{getContestDetail:t=>o(e.STUDENT.CONTEST_DETAIL(t)),getContestRooms:t=>o(e.STUDENT.CONTEST_ROOMS(t)),registerRoom:(t,n)=>o(e.STUDENT.CONTEST_ROOM_REGISTER(t,n),{method:"POST"}),cancelRoomRegistration:(t,n)=>o(e.STUDENT.CONTEST_ROOM_REGISTER(t,n),{method:"DELETE"})}}export function createMockContestRoomsApi(t){const e=[...t.rooms];return{getContestDetail:async()=>t.contest,getContestRooms:async()=>e,registerRoom:async(t,n)=>{const o=e.find(t=>t.id===n);o&&(o.registrationStatus="PENDING")},cancelRoomRegistration:async(t,n)=>{const o=e.find(t=>t.id===n);o&&(o.registrationStatus=null)}}}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ApiData } from './exam-entry.types';
|
|
2
|
+
export type ContestRoomVisibility = 'public' | 'internal' | 'private';
|
|
3
|
+
export type ContestRoomRegistrationStatus = 'PENDING' | 'APPROVED' | 'REJECTED';
|
|
4
|
+
/** Contest summary needed by the rooms list header/gating. */
|
|
5
|
+
export interface ContestRoomsContestDetail {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
status?: 'UPCOMING' | 'IN_PROGRESS' | 'COMPLETED';
|
|
9
|
+
allowMultipleSubmissions?: boolean | null;
|
|
10
|
+
}
|
|
11
|
+
/** Room item as listed on `/student/contests/:contestId/rooms`. */
|
|
12
|
+
export interface ContestRoomListItem {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
code: string;
|
|
16
|
+
examName: string;
|
|
17
|
+
status: 'UPCOMING' | 'IN_PROGRESS' | 'COMPLETED';
|
|
18
|
+
startDatetime?: string;
|
|
19
|
+
endDatetime?: string;
|
|
20
|
+
isLocked: boolean;
|
|
21
|
+
hasAttempt: boolean;
|
|
22
|
+
attemptStatus?: string;
|
|
23
|
+
isCompleted?: boolean;
|
|
24
|
+
visibility?: ContestRoomVisibility;
|
|
25
|
+
registrationStatus?: ContestRoomRegistrationStatus | null;
|
|
26
|
+
registrationCount?: number;
|
|
27
|
+
allowMultipleSubmissions?: boolean | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* API surface the rooms-list container depends on. Methods may return either
|
|
31
|
+
* the raw value or an `{ data }` envelope; the container unwraps both.
|
|
32
|
+
*/
|
|
33
|
+
export interface ContestRoomsSdkApi {
|
|
34
|
+
/** Fetch contest detail (used for header + multi-submission gating). */
|
|
35
|
+
getContestDetail: (contestId: string) => Promise<ApiData<ContestRoomsContestDetail> | ContestRoomsContestDetail>;
|
|
36
|
+
/** Fetch the list of rooms for a contest. */
|
|
37
|
+
getContestRooms: (contestId: string) => Promise<ApiData<ContestRoomListItem[]> | ContestRoomListItem[]>;
|
|
38
|
+
/** Register for an internal-visibility room. */
|
|
39
|
+
registerRoom: (contestId: string, roomId: string) => Promise<unknown>;
|
|
40
|
+
/** Cancel a pending/approved room registration. */
|
|
41
|
+
cancelRoomRegistration?: (contestId: string, roomId: string) => Promise<unknown>;
|
|
42
|
+
}
|
|
43
|
+
export interface ContestRoomsFetchApiOptions {
|
|
44
|
+
baseUrl?: string;
|
|
45
|
+
fetcher?: typeof fetch;
|
|
46
|
+
headers?: HeadersInit;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExamEntryAttempt, ExamEntryFetchApiOptions, ExamEntryRoom, ExamEntrySdkApi } from './exam-entry.types';
|
|
2
|
+
/**
|
|
3
|
+
* Default fetch-based adapter for the student exam entry flow. Mirrors the host
|
|
4
|
+
* app endpoints used by the original `/student/exams/:roomId/enter` page:
|
|
5
|
+
* room detail, active attempt by room, start/delete attempt, invite join and
|
|
6
|
+
* contest registration.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createExamEntryApi(options?: ExamEntryFetchApiOptions): ExamEntrySdkApi;
|
|
9
|
+
/** In-memory mock adapter for demo/storybook/tests. */
|
|
10
|
+
export declare function createMockExamEntryApi(seed: {
|
|
11
|
+
room: ExamEntryRoom;
|
|
12
|
+
attempt?: ExamEntryAttempt | null;
|
|
13
|
+
}): ExamEntrySdkApi;
|
|
14
|
+
export type { ExamEntryAttempt, ExamEntryContest, ExamEntryExam, ExamEntryFetchApiOptions, ExamEntryRegistrationField, ExamEntryRegistrationFormConfig, ExamEntryRoom, ExamEntrySdkApi, } from './exam-entry.types';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{API_BASE_URL as t,API_ENDPOINTS as e}from"../shared/constants/ApiConstant";export function createExamEntryApi(n={}){const o=function({baseUrl:e=t??"",fetcher:n=fetch,headers:o}){return async function(t,a){const r=await n(`${e}${t}`,{credentials:"include",...a,headers:{"Content-Type":"application/json",...o,...a?.headers}});if(!r.ok)throw new Error(await async function(t){const e=await t.json().catch(()=>{});if("object"==typeof e&&null!==e&&"message"in e&&"string"==typeof e.message)return e.message;return t.statusText}(r));if(204!==r.status)return r.json()}}(n);return{getRoom:t=>o(e.STUDENT.ROOM_DETAIL(t)),getActiveAttemptByRoom:t=>o(e.STUDENT.ATTEMPTS_ACTIVE_BY_ROOM(t)),startAttempt:t=>o(e.STUDENT.ATTEMPTS_START,{method:"POST",body:JSON.stringify({examRoomId:t})}),deleteAttempt:t=>o(e.STUDENT.ATTEMPT_DELETE(t),{method:"DELETE"}),joinViaInvite:(t,n)=>o(e.STUDENT.ROOM_JOIN_INVITE(t,n),{method:"POST"}),registerContest:(t,n)=>o(e.STUDENT.CONTEST_REGISTER(t),{method:"POST",body:JSON.stringify(n)})}}export function createMockExamEntryApi(t){let e=t.attempt??null;return{getRoom:async()=>t.room,getActiveAttemptByRoom:async()=>e,startAttempt:async()=>(e={id:`mock-attempt-${Date.now()}`,startedAt:(new Date).toISOString(),durationMinutes:t.room.exam?.durationMinutes??30,examRoomId:t.room.id,status:"IN_PROGRESS"},e),deleteAttempt:async()=>{e=null},joinViaInvite:async()=>{},registerContest:async()=>{}}}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ExamTakingAttempt } from './exam-taking.types';
|
|
2
|
+
/** Generic envelope used by the OE Exam API ({ data: ... }). */
|
|
3
|
+
export interface ApiData<T> {
|
|
4
|
+
data: T;
|
|
5
|
+
}
|
|
6
|
+
/** Registration form field config for contest rooms. */
|
|
7
|
+
export interface ExamEntryRegistrationField {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
type: string;
|
|
11
|
+
required: boolean;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
options?: string[];
|
|
14
|
+
userPropertyMapping?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Registration form config attached to a room. */
|
|
17
|
+
export interface ExamEntryRegistrationFormConfig {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
fields: ExamEntryRegistrationField[];
|
|
20
|
+
}
|
|
21
|
+
/** Exam summary nested inside a room. */
|
|
22
|
+
export interface ExamEntryExam {
|
|
23
|
+
id: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
durationMinutes?: number;
|
|
27
|
+
totalScore?: number;
|
|
28
|
+
passScore?: number;
|
|
29
|
+
level?: string;
|
|
30
|
+
theme?: string;
|
|
31
|
+
template?: {
|
|
32
|
+
theme?: string;
|
|
33
|
+
subTitle?: string;
|
|
34
|
+
title?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Contest summary nested inside a room. */
|
|
38
|
+
export interface ExamEntryContest {
|
|
39
|
+
id: string;
|
|
40
|
+
code?: string | null;
|
|
41
|
+
name?: string;
|
|
42
|
+
status?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Room as needed by the entry screen. Mirrors the fields the entry flow reads
|
|
46
|
+
* from `studentRoomsApi.getById` in the host app.
|
|
47
|
+
*/
|
|
48
|
+
export interface ExamEntryRoom {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
code?: string;
|
|
53
|
+
examId?: string;
|
|
54
|
+
startDatetime?: string;
|
|
55
|
+
endDatetime?: string;
|
|
56
|
+
isLocked?: boolean;
|
|
57
|
+
hasJoined?: boolean;
|
|
58
|
+
canAccess?: boolean;
|
|
59
|
+
allowMultipleSubmissions?: boolean;
|
|
60
|
+
hasSubmittedFormData?: boolean;
|
|
61
|
+
registrationFormConfig?: ExamEntryRegistrationFormConfig;
|
|
62
|
+
exam?: ExamEntryExam;
|
|
63
|
+
contest?: ExamEntryContest;
|
|
64
|
+
}
|
|
65
|
+
/** Attempt as needed by the entry screen. */
|
|
66
|
+
export type ExamEntryAttempt = ExamTakingAttempt;
|
|
67
|
+
/**
|
|
68
|
+
* API surface the entry container depends on. All methods may return either the
|
|
69
|
+
* raw value or an `{ data }` envelope; the container unwraps both.
|
|
70
|
+
*/
|
|
71
|
+
export interface ExamEntrySdkApi {
|
|
72
|
+
/** Fetch room detail by id. */
|
|
73
|
+
getRoom: (roomId: string) => Promise<ApiData<ExamEntryRoom> | ExamEntryRoom>;
|
|
74
|
+
/** Fetch the active (IN_PROGRESS) attempt for a specific room, or null. */
|
|
75
|
+
getActiveAttemptByRoom: (roomId: string) => Promise<ApiData<ExamEntryAttempt | null> | ExamEntryAttempt | null>;
|
|
76
|
+
/** Start a new attempt for the room. */
|
|
77
|
+
startAttempt: (roomId: string) => Promise<ApiData<ExamEntryAttempt | {
|
|
78
|
+
attempt: ExamEntryAttempt;
|
|
79
|
+
}> | ExamEntryAttempt | {
|
|
80
|
+
attempt: ExamEntryAttempt;
|
|
81
|
+
}>;
|
|
82
|
+
/** Delete an attempt (used by "restart from scratch"). */
|
|
83
|
+
deleteAttempt: (attemptId: string) => Promise<unknown>;
|
|
84
|
+
/** Join a room via an invite token (best-effort; may already be a member). */
|
|
85
|
+
joinViaInvite?: (roomId: string, inviteToken: string) => Promise<unknown>;
|
|
86
|
+
/** Register for the contest backing the room (submits registration form data). */
|
|
87
|
+
registerContest?: (contestId: string, formData: Record<string, unknown>) => Promise<unknown>;
|
|
88
|
+
}
|
|
89
|
+
export interface ExamEntryFetchApiOptions {
|
|
90
|
+
baseUrl?: string;
|
|
91
|
+
fetcher?: typeof fetch;
|
|
92
|
+
headers?: HeadersInit;
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
package/dist/api/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './exam-create';
|
|
2
2
|
export * from './exam-questions';
|
|
3
3
|
export * from './exam-taking';
|
|
4
|
+
export * from './exam-entry';
|
|
5
|
+
export * from './contest-rooms';
|
|
4
6
|
export * from './result-review';
|
|
5
7
|
export * from '../shared/constants/ApiConstant';
|
|
6
8
|
export * from '../shared/constants/QueryKeyConstant';
|
package/dist/api/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./exam-create";export*from"./exam-questions";export*from"./exam-taking";export*from"./result-review";export*from"../shared/constants/ApiConstant";export*from"../shared/constants/QueryKeyConstant";
|
|
1
|
+
export*from"./exam-create";export*from"./exam-questions";export*from"./exam-taking";export*from"./exam-entry";export*from"./contest-rooms";export*from"./result-review";export*from"../shared/constants/ApiConstant";export*from"../shared/constants/QueryKeyConstant";
|
|
@@ -4,4 +4,5 @@ export declare function createMockResultReviewApi(seed: {
|
|
|
4
4
|
detail: ResultReviewDetail;
|
|
5
5
|
attempt?: ResultReviewAttempt;
|
|
6
6
|
}): ResultReviewSdkApi;
|
|
7
|
-
export
|
|
7
|
+
export { ResultTemplateType, ReviewGradingStatus } from './result-review.types';
|
|
8
|
+
export type { ResultReviewAnswer, ResultReviewAttempt, ResultReviewDetail, ResultReviewFetchApiOptions, ResultReviewSdkApi, ResultReviewTemplatePart, ReviewAnswerValue, SpeakingRubric, SpeakingRubricCriterion, SpeakingRubricQuestion, SpeakingRubricSection, } from './result-review.types';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{API_BASE_URL as e,API_ENDPOINTS as t}from"../shared/constants/ApiConstant";export function createResultReviewApi(
|
|
1
|
+
import{API_BASE_URL as e,API_ENDPOINTS as t}from"../shared/constants/ApiConstant";export function createResultReviewApi(r={}){const i=function({baseUrl:t=e??"",fetcher:n=fetch,headers:a}){return async function(e,r){const i=await n(`${t}${e}`,{credentials:"include",...r,headers:{"Content-Type":"application/json",...a,...r?.headers}});if(!i.ok)throw new Error(await o(i));return i.json()}}(r);return{getDetailedResult:(e,a)=>i(n(t.STUDENT.RESULT_DETAIL(e),a)),getAttempt:(e,a)=>i(n(t.STUDENT.ATTEMPT_DETAIL(e),a)),exportPdf:async(n,i)=>{const s=await(r.fetcher??fetch)(`${r.baseUrl??e??""}${t.STUDENT.EXPORT_NATIVE_PDF}`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",...r.headers},body:JSON.stringify({attemptId:n,examType:i})});if(!s.ok)throw new Error(await o(s));return{blob:await s.blob(),filename:a(s.headers,n)}}}}export function createMockResultReviewApi(e){return{getDetailedResult:async()=>e.detail,getAttempt:async()=>e.attempt??{id:e.detail.id},exportPdf:async()=>({blob:new Blob(["mock pdf"],{type:"application/pdf"}),filename:`result-${e.detail.id}.pdf`})}}function n(e,t){if(!t)return e;const n=e.includes("?")?"&":"?";return`${e}${n}token=${encodeURIComponent(t)}`}function a(e,t){const n=e.get("content-disposition")??"";return n.match(/filename="?([^"]+)"?/)?.[1]??`result-${t.slice(0,8)}.pdf`}async function o(e){const t=await e.json().catch(()=>{});return"object"==typeof t&&null!==t&&"message"in t&&"string"==typeof t.message?t.message:e.statusText}export{ResultTemplateType,ReviewGradingStatus}from"./result-review.types";
|
|
@@ -1,4 +1,63 @@
|
|
|
1
1
|
export type ReviewAnswerValue = string | number | boolean | string[] | Record<string, unknown> | null;
|
|
2
|
+
export declare enum ResultTemplateType {
|
|
3
|
+
SPEAKING = "SPEAKING"
|
|
4
|
+
}
|
|
5
|
+
export declare enum ReviewGradingStatus {
|
|
6
|
+
PENDING = "PENDING",
|
|
7
|
+
QUEUED = "QUEUED",
|
|
8
|
+
PROCESSING = "PROCESSING",
|
|
9
|
+
AUTO_GRADED = "AUTO_GRADED",
|
|
10
|
+
GRADED = "GRADED",
|
|
11
|
+
COMPLETED = "COMPLETED",
|
|
12
|
+
FAILED = "FAILED",
|
|
13
|
+
MANUAL_REVIEW = "MANUAL_REVIEW",
|
|
14
|
+
NOT_ANSWERED = "NOT_ANSWERED"
|
|
15
|
+
}
|
|
16
|
+
export interface SpeakingRubricCriterion {
|
|
17
|
+
code: string;
|
|
18
|
+
score: number;
|
|
19
|
+
weightedScore?: number;
|
|
20
|
+
maxScore?: number;
|
|
21
|
+
evidence: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SpeakingRubricQuestion {
|
|
24
|
+
questionNo: number;
|
|
25
|
+
score: number;
|
|
26
|
+
maxScore: number;
|
|
27
|
+
evidence: string;
|
|
28
|
+
}
|
|
29
|
+
export interface SpeakingRubricSection {
|
|
30
|
+
code: string;
|
|
31
|
+
score: number;
|
|
32
|
+
maxScore: number;
|
|
33
|
+
questions: SpeakingRubricQuestion[];
|
|
34
|
+
}
|
|
35
|
+
export interface SpeakingRubric {
|
|
36
|
+
scoringVersion: string;
|
|
37
|
+
criteria: SpeakingRubricCriterion[];
|
|
38
|
+
sections?: SpeakingRubricSection[] | null;
|
|
39
|
+
total: number;
|
|
40
|
+
maxScore: number;
|
|
41
|
+
feedback: {
|
|
42
|
+
en: string;
|
|
43
|
+
vi: string;
|
|
44
|
+
} | null;
|
|
45
|
+
assessedAt: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface ResultReviewTemplatePart {
|
|
48
|
+
id: string | number;
|
|
49
|
+
name?: string;
|
|
50
|
+
instructions?: string;
|
|
51
|
+
audioUrl?: string;
|
|
52
|
+
sections?: Array<{
|
|
53
|
+
sectionId?: number;
|
|
54
|
+
name?: string;
|
|
55
|
+
instructions?: string;
|
|
56
|
+
startIndex?: number;
|
|
57
|
+
endIndex?: number;
|
|
58
|
+
questionType?: string;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
2
61
|
export interface ResultReviewAnswer {
|
|
3
62
|
id?: string;
|
|
4
63
|
questionId: string;
|
|
@@ -14,7 +73,11 @@ export interface ResultReviewAnswer {
|
|
|
14
73
|
partNo: number | null;
|
|
15
74
|
sectionId?: number | null;
|
|
16
75
|
isExample?: boolean;
|
|
17
|
-
gradingStatus?: string | null;
|
|
76
|
+
gradingStatus?: ReviewGradingStatus | string | null;
|
|
77
|
+
questionGroup?: {
|
|
78
|
+
id?: string | number;
|
|
79
|
+
payload?: unknown;
|
|
80
|
+
} | null;
|
|
18
81
|
explanation?: string;
|
|
19
82
|
teacherScore?: number | null;
|
|
20
83
|
teacherFeedback?: string | null;
|
|
@@ -42,6 +105,7 @@ export interface ResultReviewDetail {
|
|
|
42
105
|
rank?: number;
|
|
43
106
|
totalStudents?: number;
|
|
44
107
|
status?: string;
|
|
108
|
+
templateCode?: string | null;
|
|
45
109
|
isPendingGrading?: boolean;
|
|
46
110
|
showDetailedResults?: boolean;
|
|
47
111
|
answers: ResultReviewAnswer[];
|
|
@@ -51,6 +115,7 @@ export interface ResultReviewDetail {
|
|
|
51
115
|
percentage: number;
|
|
52
116
|
feedback: string;
|
|
53
117
|
} | undefined> | null;
|
|
118
|
+
speakingRubric?: SpeakingRubric | null;
|
|
54
119
|
}
|
|
55
120
|
export interface ResultReviewAttempt {
|
|
56
121
|
id: string;
|
|
@@ -62,8 +127,9 @@ export interface ResultReviewAttempt {
|
|
|
62
127
|
exam?: {
|
|
63
128
|
template?: {
|
|
64
129
|
title?: string;
|
|
65
|
-
type?: string;
|
|
66
|
-
|
|
130
|
+
type?: ResultTemplateType | string;
|
|
131
|
+
code?: string;
|
|
132
|
+
parts?: ResultReviewTemplatePart[];
|
|
67
133
|
};
|
|
68
134
|
};
|
|
69
135
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export{};
|
|
1
|
+
export var ResultTemplateType;!function(E){E.SPEAKING="SPEAKING"}(ResultTemplateType||(ResultTemplateType={}));export var ReviewGradingStatus;!function(E){E.PENDING="PENDING",E.QUEUED="QUEUED",E.PROCESSING="PROCESSING",E.AUTO_GRADED="AUTO_GRADED",E.GRADED="GRADED",E.COMPLETED="COMPLETED",E.FAILED="FAILED",E.MANUAL_REVIEW="MANUAL_REVIEW",E.NOT_ANSWERED="NOT_ANSWERED"}(ReviewGradingStatus||(ReviewGradingStatus={}));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface EntryRestartDialogProps {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
isStarting: boolean;
|
|
5
|
+
onContinue: () => void;
|
|
6
|
+
onRestart: () => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Shown when the student already has an IN_PROGRESS attempt for this room.
|
|
10
|
+
* Lets them resume the existing attempt or discard it and start fresh.
|
|
11
|
+
*/
|
|
12
|
+
export declare function EntryRestartDialog({ open, onOpenChange, isStarting, onContinue, onRestart, }: EntryRestartDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as n,jsxs as i,Fragment as e}from"react/jsx-runtime";import{AlertDialog as t,AlertDialogAction as r,AlertDialogCancel as l,AlertDialogContent as a,AlertDialogDescription as c,AlertDialogFooter as o,AlertDialogHeader as m,AlertDialogTitle as s}from"../../../components/ui/alert-dialog";import{Loader2 as d}from"lucide-react";export function EntryRestartDialog({open:h,onOpenChange:p,isStarting:u,onContinue:g,onRestart:b}){return n(t,{open:h,onOpenChange:p,children:i(a,{children:[i(m,{children:[n(s,{children:"Bạn đang có bài thi dở"}),n(c,{children:"Bạn đã bắt đầu làm bài thi này trước đó. Bạn muốn tiếp tục làm bài hay bắt đầu lại từ đầu?"})]}),i(o,{children:[n(l,{disabled:u,onClick:g,children:"Làm tiếp"}),n(r,{disabled:u,onClick:b,children:u?i(e,{children:[n(d,{className:"mr-2 h-4 w-4 animate-spin"}),"Đang xử lý..."]}):"Làm lại từ đầu"})]})]})})}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { StudentRoom } from '../../../shared/types/student.types';
|
|
2
|
+
import type { ExamEntrySdkApi, ExamEntryRoom } from '../../../api/exam-entry';
|
|
3
|
+
export interface StudentExamEntryPageContainerProps {
|
|
4
|
+
/** Room id from the route (`/student/exams/:roomId/enter`). */
|
|
5
|
+
roomId: string;
|
|
6
|
+
/** Optional invite token (`?invite=...`) to silently join the room first. */
|
|
7
|
+
inviteToken?: string | null;
|
|
8
|
+
/** API adapter; create one via `createExamEntryApi`. */
|
|
9
|
+
api: ExamEntrySdkApi;
|
|
10
|
+
/** Navigation callback; falls back to `window.location.href` when omitted. */
|
|
11
|
+
onNavigate?: (href: string) => void;
|
|
12
|
+
/** Path to navigate to when starting/continuing the attempt. */
|
|
13
|
+
takePath?: (roomId: string) => string;
|
|
14
|
+
/** Path to navigate to on close/back. Overrides the default contest/discover logic. */
|
|
15
|
+
closePath?: (room: StudentRoom | null) => string;
|
|
16
|
+
/** Slot to render the host app's registration form when required. */
|
|
17
|
+
renderRegistrationForm?: (props: {
|
|
18
|
+
room: ExamEntryRoom;
|
|
19
|
+
onSubmit: (formData: Record<string, unknown>) => Promise<void> | void;
|
|
20
|
+
isSubmitting: boolean;
|
|
21
|
+
}) => React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* SDK container for the fullscreen student exam entry screen. Ports the host
|
|
25
|
+
* app's `/student/exams/:roomId/enter` flow: invite join, access/exam/
|
|
26
|
+
* registration gating, room-window state machine, active-attempt resume, the
|
|
27
|
+
* restart-or-continue dialog, and the themed waiting rooms (TOEIC / Cambridge
|
|
28
|
+
* YLE / Summer Sky). Infrastructure is injected via {@link ExamEntrySdkApi},
|
|
29
|
+
* `onNavigate`, and `renderRegistrationForm`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function StudentExamEntryPageContainer({ roomId, inviteToken, api, onNavigate, takePath, closePath, renderRegistrationForm, }: StudentExamEntryPageContainerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,Fragment as t,jsxs as a}from"react/jsx-runtime";import{useState as r,useEffect as n,useCallback as i,useMemo as o}from"react";import{Dialog as s,DialogContent as l,DialogDescription as d,DialogTitle as c}from"../../../components/ui/dialog";import{t as m}from"../../../shared/lib/i18n";import{ROUTES as h}from"../../../shared/constants/routes";import{normalizeExamTheme as x,resolveThemeConfig as u}from"../../../shared/lib/themes";import{unwrap as p}from"../take/exam-taking.utils";import{toast as g}from"sonner";import{Loader2 as b,Clock as f,Trophy as v,ArrowLeft as w}from"lucide-react";import y from"next/image";import{BeforeStartCard as N,ExpiredCard as _,AlreadySubmittedCard as k}from"../../../components/features/exam-entry";import{CloseButton as T}from"../../../components/features/exam-entry/themed/components";import{ToeicWaitingRoom as E}from"../../../components/features/exam-entry/themed/ToeicWaitingRoom";import{CambridgeYleWaitingRoom as D}from"../../../components/features/exam-entry/themed/CambridgeYleWaitingRoom";import{SummerSkyWaitingRoom as S}from"../../../components/themes/summer-sky/SummerSkyWaitingRoom";import{FullscreenLoading as R}from"./fullscreen-loading";export function StudentExamEntryPageContainer({roomId:I,inviteToken:A=null,api:O,onNavigate:M,takePath:C=e=>`/student/exams/${e}/take`,closePath:B,renderRegistrationForm:W}){const L=i(e=>{M?M(e):"undefined"!=typeof window&&(window.location.href=e)},[M]),[j,F]=r(null),[z,U]=r(null),[$,P]=r("LOADING"),[X,Y]=r(0),[H,V]=r(5),[G,K]=r(!1),[Q,q]=r(!1),[J,Z]=r(null),[ee,te]=r(!1),[ae,re]=r(!1),[ne,ie]=r(!1),oe=o(()=>{const e=j?.exam?.theme,t=j?.exam?.template?.theme;return u({examTheme:e,templateTheme:t,level:j?.exam?.level})},[j?.exam,I,j]),se=o(()=>B?B(j):j?.contest?.id?h.STUDENT.CONTEST_ROOMS(j.contest.id):h.STUDENT.CONTESTS_DISCOVER,[B,j]),le=i((e,t)=>{const a=!0===e.allowMultipleSubmissions;if(t){const e=t.status;if("SUBMITTED"===e||"GRADED"===e||"PUBLISHED"===e)return a?"WITHIN_WINDOW":"ALREADY_SUBMITTED";if("IN_PROGRESS"===e)return"WITHIN_WINDOW"}const r=(new Date).getTime(),n=e.startDatetime?new Date(e.startDatetime).getTime():null,i=e.endDatetime?new Date(e.endDatetime).getTime():null;return!a&&i&&r>i?"EXPIRED":n&&r<n?"BEFORE_START":"WITHIN_WINDOW"},[]);n(()=>{(async()=>{try{if(A)try{await(O.joinViaInvite?.(I,A))}catch{}const e=await O.getRoom(I),t=p(e);if(F(t),!t.canAccess&&!A)return void P("NO_ACCESS");if(t.registrationFormConfig?.enabled&&t.registrationFormConfig.fields?.length>0&&!t.hasSubmittedFormData)return void re(!0);if(!t.exam)return void P("NO_EXAM");const a=await O.getActiveAttemptByRoom(I),r=p(a);r&&(Z(r),U(r));const n=!0===t.allowMultipleSubmissions;if(t.hasJoined&&!r&&!n)return void P("ALREADY_SUBMITTED");const i=le(t,r??null);if(P(i),"BEFORE_START"===i&&t.startDatetime){const e=new Date(t.startDatetime).getTime(),a=(new Date).getTime();Y(Math.max(0,Math.floor((e-a)/1e3)))}}catch(e){console.error("Failed to load room data:",e),g.error(m("student.exam.entry.loadError"))}})()},[I,A,le]),n(()=>{if("BEFORE_START"!==$||X<=0)return;const e=setInterval(()=>{Y(e=>e<=1?(P("WITHIN_WINDOW"),0):e-1)},1e3);return()=>clearInterval(e)},[$,X]);const de=i(async()=>{try{K(!0),await O.startAttempt(I),L(C(I))}catch(e){const t=e,a=t.response?.data?.errorCode;"ROOM_NO_EXAM"===a?P("NO_EXAM"):"ROOM_NOT_STARTED"===a?(g.error(m("student.exam.entry.notStarted")),P("BEFORE_START"),q(!1),V(5)):"ROOM_ENDED"===a?(g.error(m("student.exam.entry.expired")),P("EXPIRED")):"ATTEMPT_ALREADY_SUBMITTED"===a?(g.error("Bạn đã hoàn thành bài thi này rồi."),P("ALREADY_SUBMITTED")):g.error(m("student.exam.entry.startError")),K(!1)}},[I]),ce=i(()=>{J?te(!0):de()},[J,de]),me=i(()=>{te(!1),g.success(m("student.exam.entry.continue")),L(C(I))},[I]),he=i(async()=>{if(J)try{te(!1),K(!0),await O.deleteAttempt(J.id),Z(null),await de()}catch(e){console.error("Failed to restart attempt:",e),g.error(m("student.exam.entry.restartError")),K(!1)}},[J,de]);n(()=>{if(!Q||H<=0)return;const e=setInterval(()=>{V(e=>e<=1?(L(C(I)),0):e-1)},1e3);return()=>clearInterval(e)},[Q,H,I]);const xe=i(async e=>{if(j?.contest?.id){ie(!0);try{await(O.registerContest?.(j.contest.id,e)),re(!1);const t=await O.getRoom(I),a=p(t);if(F(a),!a.exam)return void P("NO_EXAM");const r=await O.getActiveAttemptByRoom(I),n=p(r);n&&(Z(n),U(n));const i=!0===a.allowMultipleSubmissions;if(a.hasJoined&&!n&&!i)return void P("ALREADY_SUBMITTED");const o=le(a,n??null);if(P(o),"BEFORE_START"===o&&a.startDatetime){const e=new Date(a.startDatetime).getTime(),t=(new Date).getTime();Y(Math.max(0,Math.floor((e-t)/1e3)))}}catch{g.error("Không thể gửi thông tin đăng ký. Vui lòng thử lại.")}finally{ie(!1)}}},[j?.contest?.id,I,le]);if(ae&&j?.registrationFormConfig&&W)return a(t,{children:[e(R,{}),W({room:j,onSubmit:xe,isSubmitting:ne})]});if("LOADING"===$)return e(R,{});if("NO_EXAM"===$)return e("div",{className:"fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-gradient-to-b from-sky-100 via-blue-50 to-indigo-100",children:a("div",{className:"mx-auto max-w-md p-4 text-center",children:[e("div",{className:"mb-6 text-7xl",children:"📝✨"}),e("h2",{className:"mb-3 text-2xl font-bold text-sky-700",children:"Bài thi đang được chuẩn bị"}),j&&a("div",{className:"mb-4 rounded-xl bg-white/60 px-5 py-3 text-left text-sm text-sky-700 shadow-sm backdrop-blur-sm",children:[e("p",{className:"font-semibold text-base",children:j.name}),j.code&&a("p",{className:"mt-1 text-sky-500",children:["Mã phòng: ",e("span",{className:"font-mono font-medium",children:j.code})]}),(j.startDatetime||j.endDatetime)&&a("p",{className:"mt-1 text-sky-500",children:["Thời gian thi:"," ",j.startDatetime?new Date(j.startDatetime).toLocaleString("vi-VN"):"—"," → ",j.endDatetime?new Date(j.endDatetime).toLocaleString("vi-VN"):"—"]})]}),e("p",{className:"mb-6 text-base text-sky-600",children:"Thầy cô đang chuẩn bị đề thi cho phòng này. Bạn quay lại sau nhé! 😊"}),e("button",{className:"mx-auto inline-flex items-center gap-2 rounded-2xl border-b-4 border-sky-600 bg-sky-500 px-8 py-3 text-lg font-bold text-white shadow-md transition-all hover:brightness-110 active:translate-y-0.5 active:border-b-2 active:shadow-sm",onClick:()=>window.history.back(),children:"← Quay lại"})]})});if("NO_ACCESS"===$)return e("div",{className:"fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-gradient-to-b from-red-50 via-orange-50 to-amber-50",children:a("div",{className:"mx-auto max-w-md p-4 text-center",children:[e("div",{className:"mb-6 text-7xl",children:"🔒"}),e("h2",{className:"mb-3 text-2xl font-bold text-red-600",children:"Không có quyền truy cập"}),e("p",{className:"mb-6 text-base text-red-500",children:"Bạn chưa được đăng ký vào phòng thi này. Hãy liên hệ thầy cô để được thêm vào phòng thi nhé!"}),e("button",{className:"mx-auto inline-flex items-center gap-2 rounded-2xl border-b-4 border-red-600 bg-red-500 px-8 py-3 text-lg font-bold text-white shadow-md transition-all hover:brightness-110 active:translate-y-0.5 active:border-b-2 active:shadow-sm",onClick:()=>window.history.back(),children:"← Quay lại"})]})});const ue="TOEIC"===j?.exam?.template?.subTitle,pe="SUMMER_SKY"===x(j?.exam?.theme||j?.exam?.template?.theme);if("WITHIN_WINDOW"===$&&j)return a(t,{children:[ue?e(E,{room:j,onStart:ce,isStarting:G,showCountdown:Q,autoStartCountdown:H,closeRedirectUrl:se,onClose:()=>{L(se)}}):pe?e(S,{room:j,onStart:ce,isStarting:G,onExit:()=>{L(se)}}):e(D,{room:j,onStart:ce,isStarting:G,showCountdown:Q,autoStartCountdown:H,closeRedirectUrl:se,onClose:()=>{L(se)}}),e(s,{open:ee,onOpenChange:te,children:e(l,{className:"overflow-visible border-0 bg-transparent p-0 shadow-none sm:max-w-md",children:a("div",{className:"relative overflow-visible rounded-[28px] bg-gradient-to-br from-cyan-300/25 via-white/5 to-white/0 p-px shadow-[0_30px_80px_-20px_rgba(10,75,140,0.85)] backdrop-blur-xl",children:[e("div",{className:"pointer-events-none absolute -left-16 -top-16 h-48 w-48 rounded-full bg-cyan-400/35 blur-3xl"}),e("div",{className:"pointer-events-none absolute -right-16 -bottom-16 h-48 w-48 rounded-full bg-sky-500/30 blur-3xl"}),e("div",{className:"pointer-events-none absolute left-1/2 top-0 h-24 w-1/2 -translate-x-1/2 rounded-full bg-cyan-300/15 blur-2xl"}),a("div",{className:"relative overflow-hidden rounded-[calc(28px-1px)] bg-gradient-to-b from-[#0e3d70]/97 via-[#0a4b8c]/97 to-[#072d54]/97 ring-1 ring-cyan-300/25",children:[e("div",{className:"pointer-events-none absolute inset-0 opacity-[0.04]",style:{backgroundImage:"radial-gradient(circle at 1px 1px, white 1px, transparent 0)",backgroundSize:"24px 24px"}}),e("div",{className:"pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-cyan-300/40 to-transparent"}),a("div",{className:"relative flex flex-col items-center gap-4 px-8 pt-10 pb-6",children:[a("div",{className:"relative",children:[e("div",{className:"absolute inset-0 animate-ping rounded-full bg-cyan-300/30",style:{animationDuration:"2.8s"}}),e("div",{className:"absolute -inset-3 rounded-full bg-gradient-to-br from-cyan-300/15 to-sky-500/10 blur-md"}),e("div",{className:"absolute -inset-1.5 rounded-full bg-gradient-to-br from-cyan-200/30 to-sky-400/20"}),e("div",{className:"relative flex h-16 w-16 items-center justify-center rounded-full bg-gradient-to-br from-cyan-300 via-sky-500 to-sky-700 shadow-[0_10px_30px_-6px_rgba(34,211,238,0.7),inset_0_2px_4px_rgba(255,255,255,0.4)] ring-[3px] ring-cyan-200/40",children:a("svg",{className:"h-8 w-8 text-white drop-shadow-[0_2px_3px_rgba(0,0,0,0.3)]",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",children:[e("path",{d:"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"}),e("line",{x1:"12",y1:"9",x2:"12",y2:"13"}),e("line",{x1:"12",y1:"17",x2:"12.01",y2:"17"})]})})]}),a("div",{className:"flex flex-col items-center gap-2",children:[e(c,{className:"text-center text-xl font-black tracking-tight text-white drop-shadow-[0_2px_8px_rgba(34,211,238,0.4)]",children:"Bạn đang có bài thi dở"}),e(d,{className:"max-w-sm text-center text-[13px] font-medium leading-relaxed text-cyan-100/75",children:"Bạn đã bắt đầu làm bài thi này trước đó. Bạn muốn tiếp tục làm bài hay bắt đầu lại từ đầu?"})]})]}),e("div",{className:"mx-8 h-px bg-gradient-to-r from-transparent via-cyan-300/30 to-transparent"}),a("div",{className:"relative flex flex-col-reverse gap-3 px-8 py-6 sm:flex-row sm:justify-center",children:[a("button",{onClick:me,className:"group relative inline-flex min-w-[140px] cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-full bg-white/[0.08] px-7 py-3 text-sm font-bold text-white ring-1 ring-cyan-200/30 backdrop-blur-md transition-all duration-200 hover:-translate-y-0.5 hover:bg-white/[0.14] hover:ring-cyan-200/60 hover:shadow-[0_8px_20px_-4px_rgba(34,211,238,0.3)] active:translate-y-0",children:[e("span",{className:"pointer-events-none absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/20 to-transparent transition-transform duration-700 group-hover:translate-x-full"}),e("span",{className:"relative",children:"Làm tiếp"})]}),a("button",{onClick:he,disabled:G,className:"group relative inline-flex min-w-[160px] cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-full bg-gradient-to-b from-[#ffd54a] via-[#ffb733] to-[#ff8a1f] px-7 py-3 text-sm font-black uppercase tracking-wide text-white shadow-[0_6px_0_0_#c46410,0_10px_22px_rgba(255,138,31,0.5)] ring-2 ring-orange-300/70 transition-all duration-150 hover:-translate-y-0.5 hover:brightness-110 hover:shadow-[0_8px_0_0_#c46410,0_14px_26px_rgba(255,138,31,0.55)] active:translate-y-1.5 active:shadow-[0_1px_0_0_#c46410,0_3px_8px_rgba(255,138,31,0.3)] disabled:cursor-not-allowed disabled:opacity-70",style:{textShadow:"0 2px 0 rgba(180,80,10,0.55)"},children:[e("span",{className:"pointer-events-none absolute inset-x-0 top-0 h-1/2 bg-gradient-to-b from-white/30 to-transparent"}),G?a(t,{children:[e(b,{className:"relative h-4 w-4 animate-spin"}),e("span",{className:"relative",children:"Đang xử lý..."})]}):e("span",{className:"relative",children:"Làm lại từ đầu"})]})]})]})]})})})]});const ge=oe.visuals?.backgroundFilter??"grayscale(1) blur(10px) saturate(0.85) brightness(0.9)",be=oe.visuals?.backgroundOverlay??"linear-gradient(to bottom, rgba(0, 0, 0, 0.2), transparent, rgba(0, 0, 0, 0.3))",fe=oe.visuals?.cardBackground??"radial-gradient(60% 60% at 50% 40%, rgba(184, 229, 157, 0.85) 0%, rgba(115, 213, 133, 0.85) 100%)";return a("div",{className:"fixed inset-0 z-50 flex items-center justify-center overflow-hidden",children:[e("div",{className:"absolute inset-0 scale-110 bg-cover bg-center",style:{backgroundImage:`url(${oe.assets.background})`,filter:ge}}),e("div",{className:"absolute inset-0",style:{background:be}}),a("div",{className:"relative z-10 w-[88%] max-w-2xl sm:w-[70%] md:w-[50%] lg:w-[42%]",children:[e(T,{imageSrc:oe.assets.closeButton,onClick:()=>L(se),className:"absolute -right-3 -top-7 z-30",size:72}),a("div",{className:"absolute left-1/2 top-0 z-10 -translate-x-1/2 -translate-y-1/2",children:[e(y,{src:oe.assets.titleBar,alt:"",width:260,height:56,className:"drop-shadow-lg",priority:!0,unoptimized:!0}),e("span",{className:"absolute inset-0 mt-2.5 flex items-center justify-center pb-1 text-sm font-extrabold tracking-wider text-white drop-shadow",children:oe.displayName})]}),a("div",{className:"relative overflow-hidden rounded-3xl shadow-2xl ring-1 ring-white/20",children:[e("div",{className:"h-2 w-full",style:{backgroundImage:`url(${oe.assets.cardFrame})`,backgroundSize:"100% 100%"}}),a("div",{className:"relative p-6 pt-12 md:p-8 md:pt-14",style:{background:fe},children:[a("div",{className:"mb-4 rounded-2xl bg-black/20 px-4 py-3.5 text-center ring-1 ring-white/10 backdrop-blur-sm",children:[e("h2",{className:"text-base font-extrabold tracking-wide text-white drop-shadow-sm",children:j?.name}),j?.exam?.name&&e("p",{className:"mt-0.5 text-xs font-medium text-white/80",children:j.exam.name})]}),(j?.exam?.durationMinutes||j?.exam?.totalScore)&&a("div",{className:"mb-4 grid grid-cols-2 gap-3",children:[j?.exam?.durationMinutes&&a("div",{className:"flex flex-col items-center rounded-2xl bg-black/20 px-2 py-4 ring-1 ring-white/10 backdrop-blur-sm",children:[e("div",{className:"mb-2 flex h-8 w-8 items-center justify-center rounded-full bg-white/10",children:e(f,{className:"h-4 w-4 text-white"})}),e("span",{className:"text-2xl font-extrabold text-white",children:j.exam.durationMinutes}),e("span",{className:"text-xs font-semibold text-white/80",children:"Phút"})]}),j?.exam?.totalScore&&a("div",{className:"flex flex-col items-center rounded-2xl bg-black/20 px-2 py-4 ring-1 ring-white/10 backdrop-blur-sm",children:[e("div",{className:"mb-2 flex h-8 w-8 items-center justify-center rounded-full bg-white/10",children:e(v,{className:"h-4 w-4 text-white"})}),e("span",{className:"text-2xl font-extrabold text-white",children:j.exam.totalScore}),e("span",{className:"text-xs font-semibold text-white/80",children:"Thang điểm"})]})]}),"BEFORE_START"===$&&e(N,{countdown:X,formatCountdown:e=>{const t=Math.floor(e/86400),a=Math.floor(e%86400/3600),r=Math.floor(e%3600/60),n=e%60;return t>0?`${t} ngày ${a} giờ ${r} phút`:a>0?`${a} giờ ${r} phút ${n} giây`:r>0?`${r} phút ${n} giây`:`${n} giây`}}),"EXPIRED"===$&&e(_,{}),"ALREADY_SUBMITTED"===$&&e(k,{attempt:z}),e("div",{className:"mt-5 flex justify-center",children:a("button",{className:"group relative inline-flex cursor-pointer items-center gap-2 rounded-full bg-[#0473f9] px-8 py-3 shadow-[0_4px_0_0_#1d4ed8] transition-all duration-150 hover:brightness-110 active:translate-y-[2px] active:shadow-[0_2px_0_0_#1d4ed8]",onClick:()=>L(se),children:[e(w,{className:"h-5 w-5 text-white transition-transform group-hover:-translate-x-0.5"}),e("span",{className:"text-sm font-extrabold uppercase tracking-wider text-white",children:"Quay lại"})]})}),e("div",{className:"pointer-events-none absolute -bottom-1 -left-1 opacity-60",children:e(y,{src:oe.assets.bottomLeft,alt:"",width:80,height:80,unoptimized:!0})}),e("div",{className:"pointer-events-none absolute -bottom-1 -right-1 opacity-60",children:e(y,{src:oe.assets.bottomRight,alt:"",width:80,height:80,unoptimized:!0})})]})]})]})]})}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ExamEntryAttempt, ExamEntryRoom } from '../../../api/exam-entry.types';
|
|
2
|
+
/** Fullscreen loader used while room/attempt data is being fetched. */
|
|
3
|
+
export declare function EntryLoading(): import("react").JSX.Element;
|
|
4
|
+
/** Child-friendly message screen shared by NO_EXAM / NO_ACCESS states. */
|
|
5
|
+
export declare function EntryMessage({ emoji, title, description, room, tone, onBack, }: {
|
|
6
|
+
emoji: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
room?: ExamEntryRoom | null;
|
|
10
|
+
tone: 'sky' | 'red';
|
|
11
|
+
onBack: () => void;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
/** Default (non-themed) waiting room card with exam stats and a start button. */
|
|
14
|
+
export declare function EntryWaitingRoom({ room, state, countdownLabel, attempt, isStarting, onStart, onBack, }: {
|
|
15
|
+
room: ExamEntryRoom;
|
|
16
|
+
state: 'BEFORE_START' | 'WITHIN_WINDOW' | 'EXPIRED' | 'ALREADY_SUBMITTED';
|
|
17
|
+
countdownLabel?: string;
|
|
18
|
+
attempt?: ExamEntryAttempt | null;
|
|
19
|
+
isStarting: boolean;
|
|
20
|
+
onStart: () => void;
|
|
21
|
+
onBack: () => void;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{ArrowLeft as a,Clock as l,Loader2 as n,Trophy as s}from"lucide-react";import{Button as i}from"../../../components/ui/button";export function EntryLoading(){return e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-gradient-to-b from-sky-100 via-blue-50 to-indigo-100",children:e(n,{className:"h-10 w-10 animate-spin text-sky-600"})})}export function EntryMessage({emoji:a,title:l,description:n,room:s,tone:i,onBack:r}){const o="red"===i?"text-red-600":"text-sky-700",m="red"===i?"border-red-600 bg-red-500":"border-sky-600 bg-sky-500";return e("div",{className:`fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-gradient-to-b ${"red"===i?"from-red-50 via-orange-50 to-amber-50":"from-sky-100 via-blue-50 to-indigo-100"}`,children:t("div",{className:"mx-auto max-w-md p-4 text-center",children:[e("div",{className:"mb-6 text-7xl",children:a}),e("h2",{className:`mb-3 text-2xl font-bold ${o}`,children:l}),s&&t("div",{className:"mb-4 rounded-xl bg-white/60 px-5 py-3 text-left text-sm text-sky-700 shadow-sm backdrop-blur-sm",children:[e("p",{className:"text-base font-semibold",children:s.name}),s.code&&t("p",{className:"mt-1 text-sky-500",children:["Mã phòng: ",e("span",{className:"font-mono font-medium",children:s.code})]})]}),e("p",{className:"mb-6 text-base "+("red"===i?"text-red-500":"text-sky-600"),children:n}),e("button",{className:`mx-auto inline-flex items-center gap-2 rounded-2xl border-b-4 px-8 py-3 text-lg font-bold text-white shadow-md transition-all hover:brightness-110 active:translate-y-0.5 active:border-b-2 active:shadow-sm ${m}`,onClick:r,children:"← Quay lại"})]})})}export function EntryWaitingRoom({room:o,state:m,countdownLabel:d,attempt:c,isStarting:x,onStart:b,onBack:h}){return e("div",{className:"fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-gradient-to-b from-sky-100 via-blue-50 to-indigo-100",children:t("div",{className:"relative z-10 w-[88%] max-w-lg rounded-3xl bg-white p-6 shadow-2xl md:p-8",children:[t("div",{className:"mb-4 rounded-2xl bg-slate-50 px-4 py-3.5 text-center",children:[e("h2",{className:"text-base font-extrabold tracking-wide text-slate-800",children:o.name}),o.exam?.name&&e("p",{className:"mt-0.5 text-xs font-medium text-slate-500",children:o.exam.name})]}),(o.exam?.durationMinutes||o.exam?.totalScore)&&t("div",{className:"mb-4 grid grid-cols-2 gap-3",children:[null!=o.exam?.durationMinutes&&e(r,{icon:e(l,{className:"h-4 w-4"}),value:o.exam.durationMinutes,label:"Phút"}),null!=o.exam?.totalScore&&e(r,{icon:e(s,{className:"h-4 w-4"}),value:o.exam.totalScore,label:"Thang điểm"})]}),"BEFORE_START"===m&&t("p",{className:"mb-5 text-center text-sm font-semibold text-slate-600",children:["Bài thi sẽ bắt đầu sau ",d]}),"EXPIRED"===m&&e("p",{className:"mb-5 text-center text-sm font-semibold text-red-500",children:"Bài thi đã kết thúc."}),"ALREADY_SUBMITTED"===m&&t("p",{className:"mb-5 text-center text-sm font-semibold text-slate-600",children:["Bạn đã hoàn thành bài thi này",null!=c?.totalScore?` • Điểm: ${c.totalScore}`:"","."]}),t("div",{className:"flex flex-col items-center gap-3",children:["WITHIN_WINDOW"===m&&t(i,{onClick:b,disabled:x,className:"min-w-[180px] bg-blue-600 hover:bg-blue-700",children:[x?e(n,{className:"mr-2 h-4 w-4 animate-spin"}):null,"Bắt đầu làm bài"]}),t(i,{variant:"outline",onClick:h,children:[e(a,{className:"mr-2 h-4 w-4"}),"Quay lại"]})]})]})})}function r({icon:a,value:l,label:n}){return t("div",{className:"flex flex-col items-center rounded-2xl bg-slate-50 px-2 py-4",children:[e("div",{className:"mb-2 flex h-8 w-8 items-center justify-center rounded-full bg-slate-100 text-slate-600",children:a}),e("span",{className:"text-2xl font-extrabold text-slate-800",children:l}),e("span",{className:"text-xs font-semibold text-slate-500",children:n})]})}
|