@tinyweb_dev/oe-exam-sdk 0.1.3 → 0.1.4

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.
Files changed (289) hide show
  1. package/README.md +58 -4
  2. package/dist/api/contest-rooms.d.ts +13 -0
  3. package/dist/api/contest-rooms.js +1 -0
  4. package/dist/api/contest-rooms.types.d.ts +47 -0
  5. package/dist/api/contest-rooms.types.js +1 -0
  6. package/dist/api/exam-entry.d.ts +14 -0
  7. package/dist/api/exam-entry.js +1 -0
  8. package/dist/api/exam-entry.types.d.ts +93 -0
  9. package/dist/api/exam-entry.types.js +1 -0
  10. package/dist/api/index.d.ts +2 -0
  11. package/dist/api/index.js +1 -1
  12. package/dist/components/exams/entry/EntryRestartDialog.d.ts +12 -0
  13. package/dist/components/exams/entry/EntryRestartDialog.js +1 -0
  14. package/dist/components/exams/entry/StudentExamEntryPageContainer.d.ts +31 -0
  15. package/dist/components/exams/entry/StudentExamEntryPageContainer.js +1 -0
  16. package/dist/components/exams/entry/exam-entry.states.d.ts +22 -0
  17. package/dist/components/exams/entry/exam-entry.states.js +1 -0
  18. package/dist/components/exams/entry/exam-entry.utils.d.ts +16 -0
  19. package/dist/components/exams/entry/exam-entry.utils.js +1 -0
  20. package/dist/components/exams/entry/fullscreen-loading.d.ts +5 -0
  21. package/dist/components/exams/entry/fullscreen-loading.js +1 -0
  22. package/dist/components/exams/entry/index.d.ts +7 -0
  23. package/dist/components/exams/entry/index.js +1 -0
  24. package/dist/components/exams/index.d.ts +8 -0
  25. package/dist/components/exams/index.js +1 -1
  26. package/dist/components/exams/rooms/StudentContestRoomsPageContainer.d.ts +18 -0
  27. package/dist/components/exams/rooms/StudentContestRoomsPageContainer.js +1 -0
  28. package/dist/components/exams/rooms/contest-rooms.utils.d.ts +32 -0
  29. package/dist/components/exams/rooms/contest-rooms.utils.js +1 -0
  30. package/dist/components/exams/rooms/index.d.ts +3 -0
  31. package/dist/components/exams/rooms/index.js +1 -0
  32. package/dist/components/exams/take/ExamTakingApiContext.d.ts +10 -0
  33. package/dist/components/exams/take/ExamTakingApiContext.js +1 -0
  34. package/dist/components/exams/take/ExamTakingPageContainer.d.ts +28 -4
  35. package/dist/components/exams/take/ExamTakingPageContainer.js +1 -1
  36. package/dist/components/exams/take/components/QuestionRenderer.d.ts +25 -0
  37. package/dist/components/exams/take/components/QuestionRenderer.js +1 -0
  38. package/dist/components/exams/take/components/index.d.ts +10 -0
  39. package/dist/components/exams/take/components/index.js +1 -0
  40. package/dist/components/exams/take/components/question-renderers/ArticlesViewer.d.ts +7 -0
  41. package/dist/components/exams/take/components/question-renderers/ArticlesViewer.js +1 -0
  42. package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.d.ts +22 -0
  43. package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +1 -0
  44. package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.d.ts +14 -0
  45. package/dist/components/exams/take/components/question-renderers/MoversChooseAdjectiveRenderer.js +1 -0
  46. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.d.ts +2 -0
  47. package/dist/components/exams/take/components/question-renderers/MoversChooseBestAnswerRenderer.js +1 -0
  48. package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.d.ts +3 -0
  49. package/dist/components/exams/take/components/question-renderers/MoversColoringRenderer.js +1 -0
  50. package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.d.ts +2 -0
  51. package/dist/components/exams/take/components/question-renderers/MoversFillWordHintRenderer.js +1 -0
  52. package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.d.ts +2 -0
  53. package/dist/components/exams/take/components/question-renderers/MoversGridFillRenderer.js +1 -0
  54. package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.d.ts +2 -0
  55. package/dist/components/exams/take/components/question-renderers/MoversLabelThePictureRenderer.js +1 -0
  56. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.d.ts +21 -0
  57. package/dist/components/exams/take/components/question-renderers/MoversListenAndWriteRenderer.js +1 -0
  58. package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.d.ts +19 -0
  59. package/dist/components/exams/take/components/question-renderers/MoversMatchByWritingAnswerRenderer.js +1 -0
  60. package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.d.ts +2 -0
  61. package/dist/components/exams/take/components/question-renderers/MoversPictureChooseRenderer.js +1 -0
  62. package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.d.ts +2 -0
  63. package/dist/components/exams/take/components/question-renderers/MoversPictureFillBlankChooseRenderer.js +1 -0
  64. package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.d.ts +2 -0
  65. package/dist/components/exams/take/components/question-renderers/MoversReadingPassageRenderer.js +1 -0
  66. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.d.ts +20 -0
  67. package/dist/components/exams/take/components/question-renderers/MoversTrueFalseRenderer.js +1 -0
  68. package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.d.ts +32 -0
  69. package/dist/components/exams/take/components/question-renderers/MoversWordFillParagraphRenderer.js +1 -0
  70. package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.d.ts +18 -0
  71. package/dist/components/exams/take/components/question-renderers/MoversWordFillStructuredFormRenderer.js +1 -0
  72. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.d.ts +19 -0
  73. package/dist/components/exams/take/components/question-renderers/MoversWordOrderingRenderer.js +1 -0
  74. package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.d.ts +22 -0
  75. package/dist/components/exams/take/components/question-renderers/MoversWriteSentencesRenderer.js +1 -0
  76. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.d.ts +18 -0
  77. package/dist/components/exams/take/components/question-renderers/MoversWritingRenderer.js +1 -0
  78. package/dist/components/exams/take/components/question-renderers/index.d.ts +19 -0
  79. package/dist/components/exams/take/components/question-renderers/index.js +1 -0
  80. package/dist/components/exams/take/components/speaking/FindObjectInPicture.module.css +133 -0
  81. package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.d.ts +9 -0
  82. package/dist/components/exams/take/components/speaking/SpeakingAudioPlayer.js +1 -0
  83. package/dist/components/exams/take/components/speaking/SpeakingCompareImages.module.css +153 -0
  84. package/dist/components/exams/take/components/speaking/SpeakingContentFrame.d.ts +7 -0
  85. package/dist/components/exams/take/components/speaking/SpeakingContentFrame.js +1 -0
  86. package/dist/components/exams/take/components/speaking/SpeakingDragObjects.module.css +238 -0
  87. package/dist/components/exams/take/components/speaking/SpeakingExamLayout.d.ts +27 -0
  88. package/dist/components/exams/take/components/speaking/SpeakingExamLayout.js +1 -0
  89. package/dist/components/exams/take/components/speaking/SpeakingHeader.d.ts +6 -0
  90. package/dist/components/exams/take/components/speaking/SpeakingHeader.js +1 -0
  91. package/dist/components/exams/take/components/speaking/SpeakingInfoExchange.module.css +193 -0
  92. package/dist/components/exams/take/components/speaking/SpeakingInfoExchangeSummerSky.module.css +238 -0
  93. package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.d.ts +5 -0
  94. package/dist/components/exams/take/components/speaking/SpeakingInstructionBar.js +1 -0
  95. package/dist/components/exams/take/components/speaking/SpeakingMicButton.d.ts +14 -0
  96. package/dist/components/exams/take/components/speaking/SpeakingMicButton.js +1 -0
  97. package/dist/components/exams/take/components/speaking/SpeakingNavButton.d.ts +7 -0
  98. package/dist/components/exams/take/components/speaking/SpeakingNavButton.js +1 -0
  99. package/dist/components/exams/take/components/speaking/SpeakingPartBadge.d.ts +6 -0
  100. package/dist/components/exams/take/components/speaking/SpeakingPartBadge.js +1 -0
  101. package/dist/components/exams/take/components/speaking/SpeakingReadDisplayedContent.module.css +115 -0
  102. package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.d.ts +11 -0
  103. package/dist/components/exams/take/components/speaking/SpeakingRecordingPreview.js +1 -0
  104. package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.d.ts +1 -0
  105. package/dist/components/exams/take/components/speaking/SpeakingReportErrorButton.js +1 -0
  106. package/dist/components/exams/take/components/speaking/SpeakingStoryImages.module.css +231 -0
  107. package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.d.ts +13 -0
  108. package/dist/components/exams/take/components/speaking/SpeakingVideoIntro.js +1 -0
  109. package/dist/components/exams/take/components/speaking/index.d.ts +1 -0
  110. package/dist/components/exams/take/components/speaking/index.js +1 -0
  111. package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.d.ts +16 -0
  112. package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseAnswerGroupRenderer.js +1 -0
  113. package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.d.ts +16 -0
  114. package/dist/components/exams/take/components/speaking/renderers/SpeakingChooseObjectsRenderer.js +1 -0
  115. package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.d.ts +17 -0
  116. package/dist/components/exams/take/components/speaking/renderers/SpeakingCompareImagesRenderer.js +1 -0
  117. package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.d.ts +16 -0
  118. package/dist/components/exams/take/components/speaking/renderers/SpeakingConversationRenderer.js +1 -0
  119. package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.d.ts +17 -0
  120. package/dist/components/exams/take/components/speaking/renderers/SpeakingCueCardRenderer.js +1 -0
  121. package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.d.ts +18 -0
  122. package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.js +1 -0
  123. package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.d.ts +16 -0
  124. package/dist/components/exams/take/components/speaking/renderers/SpeakingDragObjectsRenderer.js +1 -0
  125. package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.d.ts +16 -0
  126. package/dist/components/exams/take/components/speaking/renderers/SpeakingGNInterviewRenderer.js +1 -0
  127. package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.d.ts +16 -0
  128. package/dist/components/exams/take/components/speaking/renderers/SpeakingInfoExchangeRenderer.js +1 -0
  129. package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.d.ts +16 -0
  130. package/dist/components/exams/take/components/speaking/renderers/SpeakingListenAndSpeakAnswerRenderer.js +1 -0
  131. package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.d.ts +16 -0
  132. package/dist/components/exams/take/components/speaking/renderers/SpeakingOddOneOutRenderer.js +1 -0
  133. package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.d.ts +26 -0
  134. package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionListRenderer.js +1 -0
  135. package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.d.ts +32 -0
  136. package/dist/components/exams/take/components/speaking/renderers/SpeakingQuestionRenderer.js +1 -0
  137. package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.d.ts +34 -0
  138. package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.js +1 -0
  139. package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.d.ts +17 -0
  140. package/dist/components/exams/take/components/speaking/renderers/SpeakingSpontaneousQARenderer.js +1 -0
  141. package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.d.ts +17 -0
  142. package/dist/components/exams/take/components/speaking/renderers/SpeakingStoryImagesRenderer.js +1 -0
  143. package/dist/components/exams/take/components/speaking/renderers/index.d.ts +13 -0
  144. package/dist/components/exams/take/components/speaking/renderers/index.js +1 -0
  145. package/dist/components/exams/take/hooks/index.d.ts +2 -0
  146. package/dist/components/exams/take/hooks/index.js +1 -0
  147. package/dist/components/exams/take/hooks/useAnswerAutosave.d.ts +27 -0
  148. package/dist/components/exams/take/hooks/useAnswerAutosave.js +1 -0
  149. package/dist/components/exams/take/hooks/useAttemptSubmit.d.ts +29 -0
  150. package/dist/components/exams/take/hooks/useAttemptSubmit.js +1 -0
  151. package/dist/components/exams/take/hooks/useAutoSave.d.ts +20 -0
  152. package/dist/components/exams/take/hooks/useAutoSave.js +1 -0
  153. package/dist/components/exams/take/hooks/useExamCountdown.d.ts +22 -0
  154. package/dist/components/exams/take/hooks/useExamCountdown.js +1 -0
  155. package/dist/components/exams/take/hooks/useExamTimer.d.ts +21 -0
  156. package/dist/components/exams/take/hooks/useExamTimer.js +1 -0
  157. package/dist/components/exams/take/hooks/useProctoringCamera.d.ts +19 -0
  158. package/dist/components/exams/take/hooks/useProctoringCamera.js +1 -0
  159. package/dist/components/exams/take/hooks/useProctoringSnapshot.d.ts +25 -0
  160. package/dist/components/exams/take/hooks/useProctoringSnapshot.js +1 -0
  161. package/dist/components/exams/take/hooks/useSpeakingConversationAudio.d.ts +19 -0
  162. package/dist/components/exams/take/hooks/useSpeakingConversationAudio.js +1 -0
  163. package/dist/components/exams/take/hooks/useSpeakingNavigation.d.ts +37 -0
  164. package/dist/components/exams/take/hooks/useSpeakingNavigation.js +1 -0
  165. package/dist/components/exams/take/hooks/useSpeakingRecorder.d.ts +45 -5
  166. package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -1
  167. package/dist/components/exams/take/hooks/useSpeakingRendererSetup.d.ts +39 -0
  168. package/dist/components/exams/take/hooks/useSpeakingRendererSetup.js +1 -0
  169. package/dist/components/exams/take/hooks/useToeicAudio.d.ts +17 -0
  170. package/dist/components/exams/take/hooks/useToeicAudio.js +1 -0
  171. package/dist/components/exams/take/index.d.ts +9 -1
  172. package/dist/components/exams/take/index.js +1 -1
  173. package/dist/components/exams/take/types/speaking-cue-card.types.d.ts +10 -0
  174. package/dist/components/exams/take/types/speaking-cue-card.types.js +1 -0
  175. package/dist/components/exams/take/types/speaking.types.d.ts +153 -0
  176. package/dist/components/exams/take/types/speaking.types.js +1 -0
  177. package/dist/components/exams/take/types.d.ts +286 -1
  178. package/dist/components/exams/take/utils/answer-transformers.d.ts +26 -0
  179. package/dist/components/exams/take/utils/answer-transformers.js +1 -0
  180. package/dist/components/exams/take/utils/coloring-assignments.d.ts +5 -0
  181. package/dist/components/exams/take/utils/coloring-assignments.js +1 -0
  182. package/dist/components/exams/take/utils/question-transformers.d.ts +362 -0
  183. package/dist/components/exams/take/utils/question-transformers.js +1 -0
  184. package/dist/components/features/exam-entry/entry-state-cards.d.ts +19 -0
  185. package/dist/components/features/exam-entry/entry-state-cards.js +1 -0
  186. package/dist/components/features/exam-entry/index.d.ts +2 -0
  187. package/dist/components/features/exam-entry/index.js +1 -0
  188. package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.d.ts +12 -0
  189. package/dist/components/features/exam-entry/themed/CambridgeYleWaitingRoom.js +1 -0
  190. package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.d.ts +14 -0
  191. package/dist/components/features/exam-entry/themed/ThemedWaitingRoom.js +1 -0
  192. package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.d.ts +16 -0
  193. package/dist/components/features/exam-entry/themed/ToeicWaitingRoom.js +1 -0
  194. package/dist/components/features/exam-entry/themed/components/CloseButton.d.ts +9 -0
  195. package/dist/components/features/exam-entry/themed/components/CloseButton.js +1 -0
  196. package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.d.ts +13 -0
  197. package/dist/components/features/exam-entry/themed/components/ExamInfoPanel.js +1 -0
  198. package/dist/components/features/exam-entry/themed/components/ModeSelector.d.ts +7 -0
  199. package/dist/components/features/exam-entry/themed/components/ModeSelector.js +1 -0
  200. package/dist/components/features/exam-entry/themed/components/StartButton.d.ts +11 -0
  201. package/dist/components/features/exam-entry/themed/components/StartButton.js +1 -0
  202. package/dist/components/features/exam-entry/themed/components/index.d.ts +4 -0
  203. package/dist/components/features/exam-entry/themed/components/index.js +1 -0
  204. package/dist/components/features/exam-entry/themed/index.d.ts +1 -0
  205. package/dist/components/features/exam-entry/themed/index.js +1 -0
  206. package/dist/components/results/ResultReviewPageContainer.d.ts +3 -1
  207. package/dist/components/results/ResultReviewPageContainer.js +1 -1
  208. package/dist/components/results/hooks/useResultReviewPolling.d.ts +27 -0
  209. package/dist/components/results/hooks/useResultReviewPolling.js +1 -0
  210. package/dist/components/results/index.d.ts +3 -0
  211. package/dist/components/results/index.js +1 -1
  212. package/dist/components/results/review-status.utils.d.ts +12 -0
  213. package/dist/components/results/review-status.utils.js +1 -0
  214. package/dist/components/shared/audio/SpeakingAudioPlayer.d.ts +12 -0
  215. package/dist/components/shared/audio/SpeakingAudioPlayer.js +1 -0
  216. package/dist/components/shared/audio/SpeakingAudioPlayer.module.css +115 -0
  217. package/dist/components/shared/audio/assets/fr-image-part1-border.png +0 -0
  218. package/dist/components/shared/audio/assets/ic-speaker.webp +0 -0
  219. package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.d.ts +11 -3
  220. package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -1
  221. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.d.ts +5 -1
  222. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -1
  223. package/dist/components/themes/index.d.ts +11 -0
  224. package/dist/components/themes/index.js +1 -1
  225. package/dist/components/themes/spaceship/SpaceshipAudioPlayer.d.ts +8 -0
  226. package/dist/components/themes/spaceship/SpaceshipAudioPlayer.js +1 -0
  227. package/dist/components/themes/spaceship/SpaceshipLayout.d.ts +2 -0
  228. package/dist/components/themes/spaceship/SpaceshipLayout.js +1 -0
  229. package/dist/components/themes/spaceship/assets/images/X-sunset.png +0 -0
  230. package/dist/components/themes/spaceship/assets/images/X.webp +0 -0
  231. package/dist/components/themes/spaceship/assets/images/audio-bar.webp +0 -0
  232. package/dist/components/themes/spaceship/assets/images/background.webp +0 -0
  233. package/dist/components/themes/spaceship/assets/images/bg_sunset.png +0 -0
  234. package/dist/components/themes/spaceship/assets/images/big-blue-badge-sunset.png +0 -0
  235. package/dist/components/themes/spaceship/assets/images/big-blue-badge.png +0 -0
  236. package/dist/components/themes/spaceship/assets/images/big-blue-badge.webp +0 -0
  237. package/dist/components/themes/spaceship/assets/images/bingo.webp +0 -0
  238. package/dist/components/themes/spaceship/assets/images/blue-badge-sunset.png +0 -0
  239. package/dist/components/themes/spaceship/assets/images/blue-badge.png +0 -0
  240. package/dist/components/themes/spaceship/assets/images/blue-wrapper-sunset.png +0 -0
  241. package/dist/components/themes/spaceship/assets/images/blue-wrapper.webp +0 -0
  242. package/dist/components/themes/spaceship/assets/images/board_red.png +0 -0
  243. package/dist/components/themes/spaceship/assets/images/button_feedback_vi.png +0 -0
  244. package/dist/components/themes/spaceship/assets/images/circle-btn-right-sunset.png +0 -0
  245. package/dist/components/themes/spaceship/assets/images/circle-btn-right.png +0 -0
  246. package/dist/components/themes/spaceship/assets/images/circle-btn-sunset.png +0 -0
  247. package/dist/components/themes/spaceship/assets/images/circle-btn.webp +0 -0
  248. package/dist/components/themes/spaceship/assets/images/focus-vector.webp +0 -0
  249. package/dist/components/themes/spaceship/assets/images/head-phone_red.png +0 -0
  250. package/dist/components/themes/spaceship/assets/images/ic-mute-sunset.png +0 -0
  251. package/dist/components/themes/spaceship/assets/images/ic-mute.png +0 -0
  252. package/dist/components/themes/spaceship/assets/images/ic-play-sunset.png +0 -0
  253. package/dist/components/themes/spaceship/assets/images/ic-play-violet.webp +0 -0
  254. package/dist/components/themes/spaceship/assets/images/ic-play.webp +0 -0
  255. package/dist/components/themes/spaceship/assets/images/micro.png +0 -0
  256. package/dist/components/themes/spaceship/assets/images/next-small.webp +0 -0
  257. package/dist/components/themes/spaceship/assets/images/next-sunset.png +0 -0
  258. package/dist/components/themes/spaceship/assets/images/next.webp +0 -0
  259. package/dist/components/themes/spaceship/assets/images/purple-circle.webp +0 -0
  260. package/dist/components/themes/spaceship/assets/images/question-badge-sunset.png +0 -0
  261. package/dist/components/themes/spaceship/assets/images/question-badge.webp +0 -0
  262. package/dist/components/themes/spaceship/assets/images/red-badge-sunset.png +0 -0
  263. package/dist/components/themes/spaceship/assets/images/red-badge.webp +0 -0
  264. package/dist/components/themes/spaceship/assets/images/red-wrapper-sunset.png +0 -0
  265. package/dist/components/themes/spaceship/assets/images/red-wrapper.webp +0 -0
  266. package/dist/components/themes/spaceship/assets/images/robot-avatar.png +0 -0
  267. package/dist/components/themes/spaceship/assets/images/sound_red.png +0 -0
  268. package/dist/components/themes/spaceship/assets/images/student-avatar.png +0 -0
  269. package/dist/components/themes/spaceship/assets/images/teacher-play-sunset.png +0 -0
  270. package/dist/components/themes/spaceship/assets/images/teacher-play.webp +0 -0
  271. package/dist/components/themes/spaceship/assets/images/timer-badge.png +0 -0
  272. package/dist/components/themes/spaceship/assets/styles/SpaceshipLayout.module.css +347 -0
  273. package/dist/components/themes/spaceship/assets/styles/SpaceshipWaitingRoom.module.css +395 -0
  274. package/dist/components/ui/badge.d.ts +1 -1
  275. package/dist/index.d.ts +4 -4
  276. package/dist/index.js +1 -1
  277. package/dist/shared/lib/stores/examTakingStore.d.ts +54 -0
  278. package/dist/shared/lib/stores/examTakingStore.js +1 -0
  279. package/dist/shared/lib/stores/gradingTransientStore.d.ts +23 -0
  280. package/dist/shared/lib/stores/gradingTransientStore.js +1 -0
  281. package/dist/shared/lib/themes/config/starters.config.d.ts +3 -0
  282. package/dist/shared/lib/themes/config/starters.config.js +1 -0
  283. package/dist/shared/lib/themes/index.d.ts +18 -0
  284. package/dist/shared/lib/themes/index.js +1 -0
  285. package/dist/shared/lib/themes/types.d.ts +39 -0
  286. package/dist/shared/lib/themes/types.js +1 -0
  287. package/dist/shared/lib/toast.d.ts +19 -0
  288. package/dist/shared/lib/toast.js +1 -0
  289. package/package.json +10 -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 ExamTakePage({ examId, router }) {
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
- examId={examId}
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/take # màn làm bài + renderer generic
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`:
@@ -129,12 +180,15 @@ Renderer câu hỏi trong màn làm bài hiện là `GenericQuestionRenderer`.
129
180
  attemptId={attemptId}
130
181
  token={token}
131
182
  api={createResultReviewApi({ baseUrl })}
183
+ isPrintMode={isPrintMode}
132
184
  topActionsSlot={null}
133
185
  beforeReportSlot={null}
134
186
  afterReportSlot={null}
135
187
  />
136
188
  ```
137
189
 
190
+ Container tự poll lại kết quả khi còn câu đang chấm hoặc LLM assessment chưa sẵn sàng, dừng poll ở `isPrintMode` và phát tín hiệu `body[data-pdf-ready="true"]` cho headless PDF renderer. Lỗi 403 "kết quả chưa công khai" được nhận diện qua helper `isResultNotPublic` và hiển thị màn thông báo riêng thay vì toast lỗi.
191
+
138
192
  Các phần phụ thuộc app chính như gift dialog, upgrade modal, summary video/remotion nên truyền qua slot từ host app.
139
193
 
140
194
  ## MCP server
@@ -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{};
@@ -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";
@@ -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})]})}
@@ -0,0 +1,16 @@
1
+ import type { ApiData, ExamEntryAttempt, ExamEntryRoom } from '../../../api/exam-entry.types';
2
+ /** Entry screen state machine, mirrors the host app's EntryState. */
3
+ export type ExamEntryState = 'LOADING' | 'BEFORE_START' | 'WITHIN_WINDOW' | 'EXPIRED' | 'ALREADY_SUBMITTED' | 'NO_EXAM' | 'NO_ACCESS' | 'REGISTRATION_REQUIRED';
4
+ export declare function unwrap<T>(value: ApiData<T> | T): T;
5
+ export declare function unwrapAttempt(value: ExamEntryAttempt | {
6
+ attempt: ExamEntryAttempt;
7
+ }): ExamEntryAttempt;
8
+ /**
9
+ * Determine the entry state from room + attempt data. Ported from the host
10
+ * app's `determineState`, including the `allowMultipleSubmissions` handling.
11
+ */
12
+ export declare function determineEntryState(room: ExamEntryRoom, attempt: ExamEntryAttempt | null): ExamEntryState;
13
+ /** Seconds remaining until a room's start time (clamped at 0). */
14
+ export declare function getSecondsUntilStart(room: ExamEntryRoom): number;
15
+ /** Human-friendly countdown string (days/hours/minutes/seconds). */
16
+ export declare function formatCountdown(seconds: number): string;
@@ -0,0 +1 @@
1
+ const t=new Set(["SUBMITTED","GRADED","PUBLISHED"]);export function unwrap(t){return function(t){return"object"==typeof t&&null!==t&&"data"in t}(t)?t.data:t}export function unwrapAttempt(t){return"attempt"in t?t.attempt:t}export function determineEntryState(e,n){const r=!0===e.allowMultipleSubmissions;if(n){const e=n.status??"";if(t.has(e))return r?"WITHIN_WINDOW":"ALREADY_SUBMITTED";if("IN_PROGRESS"===e)return"WITHIN_WINDOW"}const a=Date.now(),o=e.startDatetime?new Date(e.startDatetime).getTime():null,i=e.endDatetime?new Date(e.endDatetime).getTime():null;return!r&&i&&a>i?"EXPIRED":o&&a<o?"BEFORE_START":"WITHIN_WINDOW"}export function getSecondsUntilStart(t){if(!t.startDatetime)return 0;const e=new Date(t.startDatetime).getTime();return Math.max(0,Math.floor((e-Date.now())/1e3))}export function formatCountdown(t){const e=Math.floor(t/86400),n=Math.floor(t%86400/3600),r=Math.floor(t%3600/60),a=t%60;return e>0?`${e} ngày ${n} giờ ${r} phút`:n>0?`${n} giờ ${r} phút ${a} giây`:r>0?`${r} phút ${a} giây`:`${a} giây`}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Fullscreen loading component for exam entry/taking.
3
+ * Matches the themed waiting-room visual style with a blurred background.
4
+ */
5
+ export declare function FullscreenLoading(): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{Loader2 as s}from"lucide-react";import{getTheme as a}from"../../../shared/lib/themes";export function FullscreenLoading(){const r=a(),i=r.visuals?.backgroundFilter??"grayscale(1) blur(8px) saturate(0.85) brightness(0.9)",l=r.visuals?.backgroundOverlay??"linear-gradient(to bottom, rgba(0, 0, 0, 0.1), transparent, rgba(0, 0, 0, 0.2))";return t("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(${r.assets.background})`,filter:i}}),e("div",{className:"absolute inset-0",style:{background:l}}),t("div",{className:"relative z-10 flex flex-col items-center gap-4",children:[e(s,{className:"h-12 w-12 animate-spin text-white drop-shadow-lg"}),e("p",{className:"text-lg font-medium text-white drop-shadow-lg",children:"Đang tải..."})]})]})}
@@ -0,0 +1,7 @@
1
+ export { StudentExamEntryPageContainer } from './StudentExamEntryPageContainer';
2
+ export type { StudentExamEntryPageContainerProps } from './StudentExamEntryPageContainer';
3
+ export { EntryRestartDialog } from './EntryRestartDialog';
4
+ export type { EntryRestartDialogProps } from './EntryRestartDialog';
5
+ export { EntryLoading, EntryMessage, EntryWaitingRoom } from './exam-entry.states';
6
+ export { determineEntryState, formatCountdown, getSecondsUntilStart, unwrap, unwrapAttempt, } from './exam-entry.utils';
7
+ export type { ExamEntryState } from './exam-entry.utils';
@@ -0,0 +1 @@
1
+ export{StudentExamEntryPageContainer}from"./StudentExamEntryPageContainer";export{EntryRestartDialog}from"./EntryRestartDialog";export{EntryLoading,EntryMessage,EntryWaitingRoom}from"./exam-entry.states";export{determineEntryState,formatCountdown,getSecondsUntilStart,unwrap,unwrapAttempt}from"./exam-entry.utils";
@@ -1,4 +1,12 @@
1
1
  export * from './take';
2
+ export { StudentExamEntryPageContainer } from './entry';
3
+ export type { StudentExamEntryPageContainerProps } from './entry';
4
+ export { EntryRestartDialog, EntryLoading, EntryMessage, EntryWaitingRoom } from './entry';
5
+ export type { EntryRestartDialogProps } from './entry';
6
+ export { determineEntryState, formatCountdown, getSecondsUntilStart } from './entry';
7
+ export type { ExamEntryState } from './entry';
8
+ export { StudentContestRoomsPageContainer } from './rooms';
9
+ export type { StudentContestRoomsPageContainerProps } from './rooms';
2
10
  export { CreateExamPageContainer } from './CreateExamPageContainer';
3
11
  export type { CreateExamPageContainerProps } from './CreateExamPageContainer';
4
12
  export { CreateExamInfoForm } from './CreateExamInfoForm';
@@ -1 +1 @@
1
- export*from"./take";export{CreateExamPageContainer}from"./CreateExamPageContainer";export{CreateExamInfoForm}from"./CreateExamInfoForm";export{TemplateSelector}from"./TemplateSelector";export{ExamCreator}from"./ExamCreator";export{ExamPartTabs}from"./ExamPartTabs";export{ExamQuestionGrid}from"./ExamQuestionGrid";export{PartAudioUploader}from"./PartAudioUploader";export{createExamQuestionsApi,createExamQuestionsFetchApi,createMockExamQuestionsApi}from"./api";export{ExamQuestionsPageContainer}from"./ExamQuestionsPageContainer";export{UnsavedChangesDialog}from"./UnsavedChangesDialog";export{CompletionDialog}from"./CompletionDialog";export{EXAM_DIFFICULTY,ExamViewDialog}from"./ExamViewDialog";export{ExamPreviewDialog}from"./ExamPreviewDialog";export{QuestionStatus,clearQuestionStatuses,getQuestionStatus,initializeQuestionStatusesFromAPI,setQuestionStatus}from"./exam-question-status";
1
+ export*from"./take";export{StudentExamEntryPageContainer}from"./entry";export{EntryRestartDialog,EntryLoading,EntryMessage,EntryWaitingRoom}from"./entry";export{determineEntryState,formatCountdown,getSecondsUntilStart}from"./entry";export{StudentContestRoomsPageContainer}from"./rooms";export{CreateExamPageContainer}from"./CreateExamPageContainer";export{CreateExamInfoForm}from"./CreateExamInfoForm";export{TemplateSelector}from"./TemplateSelector";export{ExamCreator}from"./ExamCreator";export{ExamPartTabs}from"./ExamPartTabs";export{ExamQuestionGrid}from"./ExamQuestionGrid";export{PartAudioUploader}from"./PartAudioUploader";export{createExamQuestionsApi,createExamQuestionsFetchApi,createMockExamQuestionsApi}from"./api";export{ExamQuestionsPageContainer}from"./ExamQuestionsPageContainer";export{UnsavedChangesDialog}from"./UnsavedChangesDialog";export{CompletionDialog}from"./CompletionDialog";export{EXAM_DIFFICULTY,ExamViewDialog}from"./ExamViewDialog";export{ExamPreviewDialog}from"./ExamPreviewDialog";export{QuestionStatus,clearQuestionStatuses,getQuestionStatus,initializeQuestionStatusesFromAPI,setQuestionStatus}from"./exam-question-status";
@@ -0,0 +1,18 @@
1
+ import type { ContestRoomsSdkApi } from '../../../api/contest-rooms.types';
2
+ export interface StudentContestRoomsPageContainerProps {
3
+ /** Contest id from the route (`/student/contests/:contestId/rooms`). */
4
+ contestId: string;
5
+ /** API adapter; create one via `createContestRoomsApi`. */
6
+ api: ContestRoomsSdkApi;
7
+ /** Navigation callback; falls back to `window.location.href` when omitted. */
8
+ onNavigate?: (href: string) => void;
9
+ /** Build the exam-entry path from a roomId. */
10
+ enterPath?: (roomId: string) => string;
11
+ }
12
+ /**
13
+ * Renders the student contest rooms-list screen
14
+ * (`/student/contests/:contestId/rooms`). Fetches rooms + contest detail via
15
+ * the injected `api`, handles search/filter, room registration and
16
+ * cancellation, and navigates to the exam-entry screen on room selection.
17
+ */
18
+ export declare function StudentContestRoomsPageContainer({ contestId, api, onNavigate, enterPath, }: StudentContestRoomsPageContainerProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as t,Fragment as a}from"react/jsx-runtime";import{useEffect as r,useMemo as s,useState as l}from"react";import{Building2 as n,CheckCircle2 as i,ClipboardCheck as d,Clock as o,DoorOpen as c,Globe as m,Loader2 as h,Lock as x,PlayCircle as g,Search as b,Timer as f,Users as p,XCircle as u}from"lucide-react";import{Badge as N}from"../../../components/ui/badge";import{Button as y}from"../../../components/ui/button";import{Input as w}from"../../../components/ui/input";import{Skeleton as v}from"../../../components/ui/skeleton";import{useConfirm as k}from"../../../components/ui/confirm-dialog";import{canCancelRegistration as C,canEnterRoom as D,canRetakeRoom as S,getCountdownParts as E,getRoomSortPriority as R,hasRegistrationStatus as P,isUserEngaged as T,needsRegisterAndEnter as j,needsRegistration as A,unwrap as I}from"./contest-rooms.utils";function L({targetDate:s}){const[n,i]=l(()=>E(s));return r(()=>{const e=setInterval(()=>i(E(s)),1e3);return()=>clearInterval(e)},[s]),n?t("div",{className:"flex items-center gap-1.5",children:[n.days>0&&t(a,{children:[t("div",{className:"flex min-w-[36px] flex-col items-center rounded-md bg-white/80 px-1.5 py-1 shadow-sm",children:[e("span",{className:"text-sm font-bold leading-none text-[#07aff0]",children:n.days}),e("span",{className:"text-[9px] text-gray-400",children:"ngày"})]}),e("span",{className:"text-sm font-bold text-[#07aff0]/40",children:":"})]}),t("div",{className:"flex min-w-[36px] flex-col items-center rounded-md bg-white/80 px-1.5 py-1 shadow-sm",children:[e("span",{className:"text-sm font-bold leading-none text-[#07aff0]",children:String(n.hours).padStart(2,"0")}),e("span",{className:"text-[9px] text-gray-400",children:"giờ"})]}),e("span",{className:"text-sm font-bold text-[#07aff0]/40",children:":"}),t("div",{className:"flex min-w-[36px] flex-col items-center rounded-md bg-white/80 px-1.5 py-1 shadow-sm",children:[e("span",{className:"text-sm font-bold leading-none text-[#07aff0]",children:String(n.minutes).padStart(2,"0")}),e("span",{className:"text-[9px] text-gray-400",children:"phút"})]}),e("span",{className:"text-sm font-bold text-[#07aff0]/40",children:":"}),t("div",{className:"flex min-w-[36px] flex-col items-center rounded-md bg-white/80 px-1.5 py-1 shadow-sm",children:[e("span",{className:"text-sm font-bold leading-none text-[#07aff0]",children:String(n.seconds).padStart(2,"0")}),e("span",{className:"text-[9px] text-gray-400",children:"giây"})]})]}):null}function O({room:a,contest:r}){const s=function(e,t){const a=S(e,t);return e.isCompleted&&!a?{label:"Đã hoàn thành",icon:i,className:"border-emerald-200 bg-emerald-50 text-emerald-700"}:e.hasAttempt&&"COMPLETED"===e.status?{label:"Đã thi",icon:i,className:"border-emerald-200 bg-emerald-50 text-emerald-700"}:e.hasAttempt&&"IN_PROGRESS"===e.attemptStatus?{label:"Đang thi",icon:g,className:"border-blue-200 bg-blue-50 text-blue-700"}:"internal"===e.visibility?"PENDING"===e.registrationStatus?{label:"Chờ phê duyệt",icon:o,className:"border-yellow-200 bg-yellow-50 text-yellow-700"}:"REJECTED"===e.registrationStatus?{label:"Bị từ chối",icon:u,className:"border-red-200 bg-red-50 text-red-700"}:"APPROVED"===e.registrationStatus?{label:"Đã đăng ký",icon:i,className:"border-teal-200 bg-teal-50 text-teal-700"}:{label:"Chưa đăng ký",icon:d,className:"border-gray-200 bg-gray-50 text-gray-500"}:e.hasAttempt&&!a?{label:"Đã thi",icon:i,className:"border-emerald-200 bg-emerald-50 text-emerald-700"}:e.hasAttempt&&a?{label:"Có thể thi lại",icon:c,className:"border-sky-200 bg-sky-50 text-sky-700"}:{label:"Sẵn sàng",icon:c,className:"border-sky-200 bg-sky-50 text-sky-700"}}(a,r),l=s.icon;return t(N,{variant:"outline",className:`rounded-full border-2 px-3 py-1 text-xs font-extrabold uppercase tracking-wide ${s.className}`,children:[e(l,{className:"mr-1 h-3.5 w-3.5"}),s.label]})}export function StudentContestRoomsPageContainer({contestId:g,api:N,onNavigate:E,enterPath:z=e=>`/student/exams/${e}/enter`}){const[G,M]=l(null),[V,B]=l(),[H,J]=l(!0),[$,_]=l(""),[F,K]=l("all"),[Q,U]=l(null),{confirm:X,ConfirmDialog:q}=k(),W=e=>E?E(e):window.location.href=e,Y=async()=>{J(!0);try{const[e,t]=await Promise.all([N.getContestRooms(g),N.getContestDetail(g)]);M(I(e)),B(I(t))}finally{J(!1)}};r(()=>{Y()},[g]);const Z=s(()=>{if(!G||0===G.length)return[];let e=[...G];if($.trim()){const t=$.trim().toLowerCase();e=e.filter(e=>e.name.toLowerCase().includes(t))}return"registered"===F&&(e=e.filter(e=>"APPROVED"===e.registrationStatus||"PENDING"===e.registrationStatus||e.hasAttempt)),e.sort((e,t)=>R(e)-R(t))},[G,$,F]);return H?e("div",{className:"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3",children:[1,2,3,4,5,6].map(a=>t("div",{className:"overflow-hidden rounded-xl border bg-white shadow-sm",children:[e(v,{className:"h-2 w-full"}),t("div",{className:"space-y-3 p-4",children:[e(v,{className:"h-5 w-3/4"}),e(v,{className:"h-4 w-1/2"}),e(v,{className:"h-4 w-2/3"}),t("div",{className:"flex gap-2 pt-2",children:[e(v,{className:"h-8 w-20"}),e(v,{className:"h-8 w-20"})]})]})]},a))}):G&&0!==G.length?t(a,{children:[t("div",{className:"mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",children:[t("div",{className:"relative w-full sm:max-w-xs",children:[e(b,{className:"absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"}),e(w,{type:"text",placeholder:"Tìm kiếm phòng thi...",value:$,onChange:e=>_(e.target.value),className:"h-11 rounded-2xl border-2 border-gray-200 pl-10 text-sm font-semibold focus-visible:border-[#07aff0]/40 focus-visible:ring-0"})]}),t("div",{className:"flex items-center gap-1.5 rounded-2xl border-2 border-gray-100 bg-gray-50 p-1.5",children:[e("button",{type:"button",onClick:()=>K("all"),className:"rounded-xl px-4 py-2 text-xs font-extrabold uppercase tracking-wide transition-all "+("all"===F?"border-2 border-b-4 border-[#07aff0]/30 bg-white text-[#07aff0]":"border-2 border-transparent text-gray-400 hover:text-[#07aff0]"),children:"Tất cả"}),e("button",{type:"button",onClick:()=>K("registered"),className:"rounded-xl px-4 py-2 text-xs font-extrabold uppercase tracking-wide transition-all "+("registered"===F?"border-2 border-b-4 border-[#07aff0]/30 bg-white text-[#07aff0]":"border-2 border-transparent text-gray-400 hover:text-[#07aff0]"),children:"Đã đăng ký"})]})]}),0===Z.length&&t("div",{className:"flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-gray-200 bg-gradient-to-br from-gray-50 to-white py-12",children:[e("div",{className:"flex h-16 w-16 items-center justify-center rounded-full bg-gray-100",children:e(b,{className:"h-8 w-8 text-gray-300"})}),e("p",{className:"mt-3 text-base font-medium text-gray-400",children:"Không tìm thấy phòng thi phù hợp"}),e("p",{className:"mt-1 text-sm text-gray-400",children:"Thử thay đổi từ khóa tìm kiếm hoặc bộ lọc"})]}),e("div",{className:"grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3",children:Z.map(r=>{!function(e){if(e.isLocked)return{label:"Đã khóa",className:"border-red-200 bg-red-50 text-red-700",icon:x,canEnter:!1};switch(e.status){case"IN_PROGRESS":return{label:"Đang mở",className:"border-green-200 bg-green-50 text-green-700",icon:c,canEnter:!0};case"COMPLETED":return{label:"Đã kết thúc",className:"border-gray-200 bg-gray-50 text-gray-700",icon:o,canEnter:!1};default:;}}(r);const s=S(r,V),l=!(r.isLocked||"UPCOMING"!==r.status&&r.status),b=D(r,V),w=A(r),v=P(r),k=T(r),E=Q===r.id;return e("div",{className:"group relative flex flex-col overflow-hidden rounded-3xl border-2 border-b-[6px] bg-white shadow-sm transition-all duration-150 hover:-translate-y-0.5 "+(k?"border-[#07aff0]/30":"border-gray-200"),children:t("div",{className:"flex flex-1 flex-col p-5",children:[t("div",{className:"mb-3 flex items-start justify-between gap-3",children:[t("div",{className:"min-w-0 flex-1",children:[t("h3",{className:"flex items-center gap-2 truncate text-lg font-extrabold tracking-tight text-gray-800 group-hover:text-[#07aff0]",children:[(!r.visibility||"public"===r.visibility)&&e("span",{className:"flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-xl bg-emerald-100",children:e(m,{className:"h-4 w-4 text-emerald-500"})}),"internal"===r.visibility&&e("span",{className:"flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-xl bg-amber-100",children:e(n,{className:"h-4 w-4 text-amber-500"})}),"private"===r.visibility&&e("span",{className:"flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-xl bg-gray-100",children:e(x,{className:"h-4 w-4 text-gray-400"})}),e("span",{className:"truncate",children:r.name})]}),t("p",{className:"mt-1 text-xs font-medium text-gray-400",children:["Mã: ",e("span",{className:"font-mono font-bold text-gray-500",children:r.code})]})]}),t("div",{className:"flex flex-shrink-0 items-start gap-2",children:[null!=r.registrationCount&&t("div",{className:"flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-1 text-gray-500",children:[e(p,{className:"h-3.5 w-3.5"}),e("span",{className:"text-xs font-bold",children:r.registrationCount})]}),r.startDatetime&&e("div",{className:"flex flex-col items-center overflow-hidden rounded-2xl border-2 border-gray-200 bg-white",children:t("div",{className:"flex flex-col items-center px-3 py-1.5",children:[e("span",{className:"text-xl font-extrabold leading-tight text-gray-800",children:new Date(r.startDatetime).getDate()}),t("span",{className:"text-[10px] font-bold uppercase text-gray-400",children:["Th ",new Date(r.startDatetime).getMonth()+1]})]})})]})]}),t("div",{className:"mb-3 space-y-2",children:[t("div",{className:"flex items-center gap-2 text-xs font-semibold text-gray-500",children:[e(d,{className:"h-4 w-4 flex-shrink-0 text-gray-400"}),e("span",{className:"truncate",children:r.examName})]}),r.startDatetime&&t("div",{className:"flex items-center gap-2 text-xs font-semibold text-gray-500",children:[e(o,{className:"h-4 w-4 flex-shrink-0 text-gray-400"}),t("span",{children:[new Date(r.startDatetime).toLocaleTimeString("vi-VN",{hour:"2-digit",minute:"2-digit"}),r.endDatetime&&t(a,{children:[" - ",new Date(r.endDatetime).toLocaleTimeString("vi-VN",{hour:"2-digit",minute:"2-digit"})]})]})]})]}),e("div",{className:"mb-3 flex flex-wrap items-center gap-1.5",children:e(O,{room:r,contest:V})}),!b&&!w&&!v&&l&&r.startDatetime&&t("div",{className:"mb-3 flex items-center gap-2 rounded-2xl border-2 border-[#07aff0]/15 bg-[#07aff0]/5 px-3 py-2",children:[e(f,{className:"h-4 w-4 flex-shrink-0 text-[#07aff0]"}),e("span",{className:"text-xs font-bold text-[#07aff0]",children:"Mở sau"}),e("div",{className:"ml-auto",children:e(L,{targetDate:r.startDatetime})})]}),e("div",{className:"mt-auto pt-3"}),t("div",{className:"flex flex-col gap-2",children:[t("div",{className:"flex flex-wrap items-center gap-2",children:[r.isCompleted&&!s&&t(y,{size:"sm",className:"h-11 flex-1 cursor-default rounded-2xl border-2 border-emerald-200 bg-emerald-50 text-sm font-extrabold uppercase tracking-wide text-emerald-600 hover:bg-emerald-50",disabled:!0,children:[e(i,{className:"mr-2 h-4 w-4"}),"Đã hoàn thành"]}),j(r,V)&&t(y,{size:"sm",className:"h-11 flex-1 rounded-2xl border-b-4 border-[#003d87] bg-[#07aff0] text-sm font-extrabold uppercase tracking-wide text-white shadow-none transition-all hover:bg-[#0060d0] active:translate-y-0.5 active:border-b-2",onClick:()=>(async e=>{U(e);try{await N.registerRoom(g,e),W(z(e))}finally{U(null)}})(r.id),disabled:E,children:[E?e(h,{className:"mr-2 h-4 w-4 animate-spin"}):e(c,{className:"mr-2 h-4 w-4"}),r.hasAttempt?"Đăng kí và Thi lại":"Vào phòng thi"]}),b&&!j(r,V)&&t("div",{className:"flex-1 flex flex-col gap-1.5",children:[r.hasAttempt&&e("div",{className:"rounded-xl border-2 border-amber-200/60 bg-amber-50 px-2 py-1 text-center text-xs font-bold text-amber-600",children:"Bạn đã hoàn thành bài thi"}),t(y,{size:"sm",className:"h-11 w-full rounded-2xl border-0 border-b-4 border-[#05a0db] bg-[#07aff0] text-sm font-extrabold uppercase tracking-wide text-white shadow-none transition-all hover:bg-[#06a0e0] active:translate-y-0.5 active:border-b-2",onClick:()=>W(z(r.id)),children:[e(c,{className:"mr-2 h-4 w-4"}),r.hasAttempt?"Thi lại":"Vào phòng thi"]})]}),w&&t(y,{size:"sm",className:"h-11 flex-1 rounded-2xl border-2 border-b-4 border-[#07aff0]/30 bg-white text-sm font-extrabold uppercase tracking-wide text-[#07aff0] shadow-none transition-all hover:bg-[#07aff0]/5 active:translate-y-0.5 active:border-b-2",onClick:()=>(async e=>{U(e);try{await N.registerRoom(g,e),await Y()}finally{U(null)}})(r.id),disabled:E,children:[E?e(h,{className:"mr-2 h-4 w-4 animate-spin"}):e(d,{className:"mr-2 h-4 w-4"}),"Đăng ký"]}),v&&"PENDING"===r.registrationStatus&&t(y,{size:"sm",className:"h-11 flex-1 cursor-default rounded-2xl border-2 border-yellow-200 bg-yellow-50 text-sm font-extrabold uppercase tracking-wide text-yellow-600 hover:bg-yellow-50",disabled:!0,children:[e(o,{className:"mr-2 h-4 w-4"}),"Đã gửi đăng ký"]}),v&&"REJECTED"===r.registrationStatus&&t(y,{size:"sm",className:"h-11 flex-1 cursor-default rounded-2xl border-2 border-red-200 bg-red-50 text-sm font-extrabold uppercase tracking-wide text-red-600 hover:bg-red-50",disabled:!0,children:[e(u,{className:"mr-2 h-4 w-4"}),"Bị từ chối"]})]}),C(r)&&e("button",{type:"button",className:"self-center text-[11px] font-semibold text-gray-400 underline-offset-2 hover:text-red-500 hover:underline transition-colors disabled:opacity-50",onClick:()=>(async e=>{if(await X({title:"Xác nhận hủy đăng ký",description:"Bạn có chắc chắn muốn hủy đăng ký phòng thi này không? Hành động này không thể hoàn tác.",confirmText:"Hủy đăng ký",cancelText:"Quay lại",variant:"destructive"})&&N.cancelRoomRegistration){U(e);try{await N.cancelRoomRegistration(g,e),await Y()}finally{U(null)}}})(r.id),disabled:E,children:E?"Đang hủy...":"Hủy đăng ký"})]})]})},r.id)})}),e(q,{})]}):t("div",{className:"flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-gray-200 bg-gradient-to-br from-gray-50 to-white py-16",children:[e("div",{className:"flex h-20 w-20 items-center justify-center rounded-full bg-gray-100",children:e(c,{className:"h-10 w-10 text-gray-300"})}),e("p",{className:"mt-4 text-lg font-medium text-gray-400",children:"Chưa có phòng thi nào trong kỳ thi này"})]})}
@@ -0,0 +1,32 @@
1
+ import type { ApiData } from '../../../api/exam-entry.types';
2
+ import type { ContestRoomListItem, ContestRoomsContestDetail } from '../../../api/contest-rooms.types';
3
+ export declare function unwrap<T>(value: ApiData<T> | T): T;
4
+ /** Whether a student can retake a room (room override, falling back to contest default). */
5
+ export declare function canRetakeRoom(room: ContestRoomListItem, contest?: ContestRoomsContestDetail): boolean;
6
+ /** Whether the student can enter the room right now (status + registration gating). */
7
+ export declare function canEnterRoom(room: ContestRoomListItem, contest?: ContestRoomsContestDetail): boolean;
8
+ /**
9
+ * Public/private rooms need no student-initiated registration — the student
10
+ * enters directly (the backend auto-approves participation on first attempt).
11
+ * Registration is an internal-room concept only, so this is always false and
12
+ * public/private rooms fall through to the plain "enter" button. Kept as a
13
+ * named helper so the container's button logic reads intently.
14
+ */
15
+ export declare function needsRegisterAndEnter(_room: ContestRoomListItem, _contest?: ContestRoomsContestDetail): boolean;
16
+ /** Internal room the student hasn't requested registration for yet. */
17
+ export declare function needsRegistration(room: ContestRoomListItem): boolean;
18
+ /** Internal room registration is pending/rejected (i.e. not approved yet). */
19
+ export declare function hasRegistrationStatus(room: ContestRoomListItem): boolean;
20
+ /** Whether a "cancel registration" action should be offered. */
21
+ export declare function canCancelRegistration(room: ContestRoomListItem): boolean;
22
+ /** Whether the student has any meaningful engagement with this room. */
23
+ export declare function isUserEngaged(room: ContestRoomListItem): boolean;
24
+ /** Sort priority for a room; lower = shown first. */
25
+ export declare function getRoomSortPriority(room: ContestRoomListItem): number;
26
+ /** Human-friendly seconds-remaining breakdown for a room's start countdown. */
27
+ export declare function getCountdownParts(targetIso: string): {
28
+ days: number;
29
+ hours: number;
30
+ minutes: number;
31
+ seconds: number;
32
+ } | null;
@@ -0,0 +1 @@
1
+ export function unwrap(t){return function(t){return"object"==typeof t&&null!==t&&"data"in t}(t)?t.data:t}export function canRetakeRoom(t,i){return!0===t.allowMultipleSubmissions||!1!==t.allowMultipleSubmissions&&!0===i?.allowMultipleSubmissions}export function canEnterRoom(t,i){const e=canRetakeRoom(t,i);return!(t.isCompleted&&!e)&&(!t.isLocked&&"IN_PROGRESS"===t.status&&("internal"!==t.visibility||"APPROVED"===t.registrationStatus))}export function needsRegisterAndEnter(t,i){return!1}export function needsRegistration(t){return"internal"===t.visibility&&!t.registrationStatus}export function hasRegistrationStatus(t){return"internal"===t.visibility&&!!t.registrationStatus&&"APPROVED"!==t.registrationStatus}export function canCancelRegistration(t){return!t.hasAttempt&&!t.isCompleted&&("PENDING"===t.registrationStatus||"APPROVED"===t.registrationStatus)}export function isUserEngaged(t){return!(!t.isCompleted&&!t.hasAttempt)||("APPROVED"===t.registrationStatus||"PENDING"===t.registrationStatus)}export function getRoomSortPriority(t){return t.hasAttempt&&"IN_PROGRESS"===t.attemptStatus?0:"IN_PROGRESS"!==t.status||t.isLocked||t.isCompleted||"internal"===t.visibility&&"APPROVED"!==t.registrationStatus?("internal"!==t.visibility||"APPROVED"!==t.registrationStatus||t.isCompleted)&&("internal"===t.visibility||t.hasAttempt||"UPCOMING"!==t.status)?"internal"===t.visibility&&"PENDING"===t.registrationStatus?3:t.isCompleted||t.hasAttempt?4:"internal"!==t.visibility||t.registrationStatus?"internal"===t.visibility&&"REJECTED"===t.registrationStatus?6:"COMPLETED"===t.status||t.isLocked?7:8:5:2:1}export function getCountdownParts(t){const i=new Date(t).getTime()-Date.now();return i<=0?null:{days:Math.floor(i/864e5),hours:Math.floor(i%864e5/36e5),minutes:Math.floor(i%36e5/6e4),seconds:Math.floor(i%6e4/1e3)}}