@tinyweb_dev/oe-exam-sdk 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (514) hide show
  1. package/dist/api/exam-create.js +1 -179
  2. package/dist/api/exam-create.types.js +1 -1
  3. package/dist/api/exam-questions.d.ts +1 -1
  4. package/dist/api/exam-questions.js +1 -98
  5. package/dist/api/exam-questions.types.d.ts +45 -1
  6. package/dist/api/exam-questions.types.js +1 -1
  7. package/dist/api/exam-taking.js +1 -73
  8. package/dist/api/exam-taking.types.js +1 -1
  9. package/dist/api/index.js +1 -6
  10. package/dist/api/result-review.js +1 -50
  11. package/dist/api/result-review.types.js +1 -1
  12. package/dist/components/CueCardEditor.js +1 -58
  13. package/dist/components/common/ResolvedImage.js +1 -15
  14. package/dist/components/exams/CompletionDialog.d.ts +8 -0
  15. package/dist/components/exams/CompletionDialog.js +1 -0
  16. package/dist/components/exams/CreateExamInfoForm.d.ts +4 -1
  17. package/dist/components/exams/CreateExamInfoForm.js +1 -35
  18. package/dist/components/exams/CreateExamPageContainer.d.ts +3 -0
  19. package/dist/components/exams/CreateExamPageContainer.js +1 -103
  20. package/dist/components/exams/ExamCreator.d.ts +9 -3
  21. package/dist/components/exams/ExamCreator.js +1 -89
  22. package/dist/components/exams/ExamPartTabs.d.ts +1 -1
  23. package/dist/components/exams/ExamPartTabs.js +1 -40
  24. package/dist/components/exams/ExamPreviewDialog.d.ts +20 -0
  25. package/dist/components/exams/ExamPreviewDialog.js +1 -0
  26. package/dist/components/exams/ExamQuestionGrid.d.ts +1 -1
  27. package/dist/components/exams/ExamQuestionGrid.js +1 -18
  28. package/dist/components/exams/ExamQuestionsPageContainer.d.ts +3 -0
  29. package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
  30. package/dist/components/exams/ExamViewDialog.d.ts +24 -0
  31. package/dist/components/exams/ExamViewDialog.js +1 -0
  32. package/dist/components/exams/PartAudioUploader.js +1 -9
  33. package/dist/components/exams/TemplateSelector.js +1 -66
  34. package/dist/components/exams/UnsavedChangesDialog.d.ts +8 -0
  35. package/dist/components/exams/UnsavedChangesDialog.js +1 -0
  36. package/dist/components/exams/api.d.ts +1 -1
  37. package/dist/components/exams/api.js +1 -3
  38. package/dist/components/exams/create-exam.texts.d.ts +58 -0
  39. package/dist/components/exams/create-exam.texts.js +1 -0
  40. package/dist/components/exams/create-exam.validation.js +1 -18
  41. package/dist/components/exams/exam-question-status.d.ts +9 -0
  42. package/dist/components/exams/exam-question-status.js +1 -0
  43. package/dist/components/exams/exam-template.utils.js +1 -18
  44. package/dist/components/exams/index.d.ts +11 -1
  45. package/dist/components/exams/index.js +1 -10
  46. package/dist/components/exams/take/ExamTakingPageContainer.js +1 -153
  47. package/dist/components/exams/take/GenericQuestionRenderer.js +1 -14
  48. package/dist/components/exams/take/exam-taking.utils.js +1 -88
  49. package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -24
  50. package/dist/components/exams/take/index.js +1 -2
  51. package/dist/components/exams/take/types.js +1 -1
  52. package/dist/components/index.js +1 -42
  53. package/dist/components/questions/_shared/config/question-types.config.js +1 -333
  54. package/dist/components/questions/_shared/constants/color-palette.js +1 -21
  55. package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +1 -105
  56. package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +1 -222
  57. package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +1 -140
  58. package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +1 -123
  59. package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +1 -50
  60. package/dist/components/questions/_shared/hooks/useQuestionFormState.js +1 -54
  61. package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +1 -123
  62. package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +1 -121
  63. package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +1 -70
  64. package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +1 -194
  65. package/dist/components/questions/_shared/index.js +1 -7
  66. package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
  67. package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
  68. package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +1 -3
  69. package/dist/components/questions/_shared/types/color-region.config.js +1 -1
  70. package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
  71. package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
  72. package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
  73. package/dist/components/questions/_shared/types/index.js +1 -12
  74. package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
  75. package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
  76. package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
  77. package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
  78. package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
  79. package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
  80. package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
  81. package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
  82. package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +1 -3
  83. package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
  84. package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +1 -3
  85. package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
  86. package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
  87. package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
  88. package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
  89. package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
  90. package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
  91. package/dist/components/questions/_shared/types/question-group.type.js +1 -11
  92. package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
  93. package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
  94. package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
  95. package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
  96. package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
  97. package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
  98. package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
  99. package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
  100. package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
  101. package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
  102. package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +1 -3
  103. package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
  104. package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
  105. package/dist/components/questions/_shared/utils/answer-builder.js +1 -79
  106. package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +1 -39
  107. package/dist/components/questions/_shared/utils/id-generator.js +1 -3
  108. package/dist/components/questions/_shared/utils/question-validation.js +1 -21
  109. package/dist/components/questions/components/ColorNode.js +1 -85
  110. package/dist/components/questions/components/ColorRegionClient.js +1 -545
  111. package/dist/components/questions/components/ColorRegionCreator.js +1 -420
  112. package/dist/components/questions/components/ColorRegionNode.js +1 -59
  113. package/dist/components/questions/components/CreatorGuide.js +1 -103
  114. package/dist/components/questions/components/NameNode.js +1 -26
  115. package/dist/components/questions/components/QuestionBankOption.js +1 -46
  116. package/dist/components/questions/components/QuestionBasicInfoForm.js +1 -9
  117. package/dist/components/questions/components/QuestionCommonFields.js +1 -33
  118. package/dist/components/questions/components/QuestionSearchDropdown.js +1 -258
  119. package/dist/components/questions/components/QuestionTypeSelector.js +1 -32
  120. package/dist/components/questions/components/QuestionTypeSpecificForms.js +1 -6
  121. package/dist/components/questions/components/RegionNode.js +1 -94
  122. package/dist/components/questions/components/index.js +1 -14
  123. package/dist/components/questions/creator/QuestionCreator.js +1 -90
  124. package/dist/components/questions/creator/QuestionGroupCreator.js +1 -119
  125. package/dist/components/questions/creator/dedicated-component-router.js +1 -79
  126. package/dist/components/questions/creator/index.js +1 -3
  127. package/dist/components/questions/creator/question-type-registry.js +1 -80
  128. package/dist/components/questions/groups/ArticlesGroupCard.js +1 -121
  129. package/dist/components/questions/groups/BasicQuestionGroupCard.js +1 -50
  130. package/dist/components/questions/groups/DocumentGroupCard.js +1 -114
  131. package/dist/components/questions/groups/MatchAnswerGroupCard.js +1 -25
  132. package/dist/components/questions/index.js +1 -10
  133. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +1 -37
  134. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +1 -351
  135. package/dist/components/questions/types/answer-the-question/register.js +1 -67
  136. package/dist/components/questions/types/answer-the-question/transform.js +1 -36
  137. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +1 -141
  138. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +1 -118
  139. package/dist/components/questions/types/arrange-letters-into-words/index.js +1 -2
  140. package/dist/components/questions/types/arrange-letters-into-words/register.js +1 -4
  141. package/dist/components/questions/types/arrange-letters-into-words/transform.js +1 -24
  142. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +1 -64
  143. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +1 -169
  144. package/dist/components/questions/types/choose-correct-adjective/index.js +1 -2
  145. package/dist/components/questions/types/choose-correct-adjective/register.js +1 -55
  146. package/dist/components/questions/types/choose-correct-adjective/transform.js +1 -16
  147. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -131
  148. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -581
  149. package/dist/components/questions/types/choose-the-correct-answer/index.js +1 -2
  150. package/dist/components/questions/types/choose-the-correct-answer/register.js +1 -101
  151. package/dist/components/questions/types/choose-the-correct-answer/transform.js +1 -43
  152. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -275
  153. package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +1 -448
  154. package/dist/components/questions/types/fill-in-blank/index.js +1 -2
  155. package/dist/components/questions/types/fill-in-blank/register.js +1 -138
  156. package/dist/components/questions/types/fill-in-blank/transform.js +1 -59
  157. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +1 -117
  158. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +1 -671
  159. package/dist/components/questions/types/fill-missing-words-in-grid/index.js +1 -2
  160. package/dist/components/questions/types/fill-missing-words-in-grid/register.js +1 -37
  161. package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +1 -15
  162. package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +1 -262
  163. package/dist/components/questions/types/gn-speaking-interview/register.js +1 -121
  164. package/dist/components/questions/types/gn-speaking-interview/transform.js +1 -48
  165. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +1 -16
  166. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +1 -27
  167. package/dist/components/questions/types/image-object-matching/index.js +1 -2
  168. package/dist/components/questions/types/image-object-matching/register.js +1 -3
  169. package/dist/components/questions/types/image-object-matching/transform.js +1 -2
  170. package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +1 -102
  171. package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +1 -344
  172. package/dist/components/questions/types/label-the-picture/index.js +1 -2
  173. package/dist/components/questions/types/label-the-picture/register.js +1 -54
  174. package/dist/components/questions/types/label-the-picture/transform.js +1 -28
  175. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +1 -78
  176. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +1 -232
  177. package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +1 -2
  178. package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +1 -82
  179. package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +1 -27
  180. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +1 -70
  181. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +1 -386
  182. package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +1 -2
  183. package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +1 -100
  184. package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +1 -33
  185. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +1 -106
  186. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +1 -457
  187. package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +1 -2
  188. package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +1 -110
  189. package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +1 -35
  190. package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +1 -8
  191. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +1 -22
  192. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +1 -291
  193. package/dist/components/questions/types/listen-and-speak-answer/index.js +1 -2
  194. package/dist/components/questions/types/listen-and-speak-answer/register.js +1 -84
  195. package/dist/components/questions/types/listen-and-speak-answer/transform.js +1 -27
  196. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +1 -32
  197. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +1 -295
  198. package/dist/components/questions/types/listen-and-speak-compare-images/register.js +1 -79
  199. package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +1 -27
  200. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +1 -34
  201. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +1 -285
  202. package/dist/components/questions/types/listen-and-speak-image-group/register.js +1 -81
  203. package/dist/components/questions/types/listen-and-speak-image-group/transform.js +1 -26
  204. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +1 -38
  205. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +1 -385
  206. package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +1 -87
  207. package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +1 -32
  208. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +1 -26
  209. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +1 -247
  210. package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +1 -59
  211. package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +1 -18
  212. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +1 -26
  213. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +1 -256
  214. package/dist/components/questions/types/listen-and-speak-question-list/register.js +1 -81
  215. package/dist/components/questions/types/listen-and-speak-question-list/transform.js +1 -26
  216. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +1 -26
  217. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +1 -230
  218. package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +1 -56
  219. package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +1 -16
  220. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +1 -72
  221. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +1 -230
  222. package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
  223. package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +1 -66
  224. package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +1 -42
  225. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +1 -96
  226. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +1 -238
  227. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +1 -2
  228. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +1 -76
  229. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +1 -19
  230. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +1 -134
  231. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +1 -617
  232. package/dist/components/questions/types/look-picture-fill-word-hint/index.js +1 -2
  233. package/dist/components/questions/types/look-picture-fill-word-hint/register.js +1 -82
  234. package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +1 -29
  235. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +1 -38
  236. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +1 -359
  237. package/dist/components/questions/types/match-by-writing-answer/index.js +1 -3
  238. package/dist/components/questions/types/match-by-writing-answer/register.js +1 -58
  239. package/dist/components/questions/types/match-by-writing-answer/transform.js +1 -61
  240. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +1 -16
  241. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +1 -23
  242. package/dist/components/questions/types/read-and-color-objects/index.js +1 -2
  243. package/dist/components/questions/types/read-and-color-objects/register.js +1 -3
  244. package/dist/components/questions/types/read-and-color-objects/transform.js +1 -36
  245. package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +1 -125
  246. package/dist/components/questions/types/read-displayed-content/register.js +1 -64
  247. package/dist/components/questions/types/read-displayed-content/transform.js +1 -12
  248. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +1 -64
  249. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +1 -511
  250. package/dist/components/questions/types/read-passage-and-answer-questions/index.js +1 -2
  251. package/dist/components/questions/types/read-passage-and-answer-questions/register.js +1 -58
  252. package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +1 -28
  253. package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +1 -373
  254. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -41
  255. package/dist/components/questions/types/speaking-conversation/register.js +1 -110
  256. package/dist/components/questions/types/speaking-conversation/transform.js +1 -34
  257. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -74
  258. package/dist/components/questions/types/speaking-cue-card/register.js +1 -52
  259. package/dist/components/questions/types/speaking-cue-card/transform.js +1 -12
  260. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +1 -10
  261. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +1 -161
  262. package/dist/components/questions/types/speaking-describe-image/index.js +1 -2
  263. package/dist/components/questions/types/speaking-describe-image/register.js +1 -69
  264. package/dist/components/questions/types/speaking-describe-image/transform.js +1 -16
  265. package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +1 -234
  266. package/dist/components/questions/types/spontaneous-qa/register.js +1 -93
  267. package/dist/components/questions/types/spontaneous-qa/transform.js +1 -34
  268. package/dist/components/questions/types/true-false/TrueFalseClient.js +1 -75
  269. package/dist/components/questions/types/true-false/TrueFalseCreator.js +1 -244
  270. package/dist/components/questions/types/true-false/index.js +1 -2
  271. package/dist/components/questions/types/true-false/register.js +1 -77
  272. package/dist/components/questions/types/true-false/transform.js +1 -32
  273. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +1 -233
  274. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +1 -631
  275. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +1 -28
  276. package/dist/components/questions/types/word-fill-paragraph/index.js +1 -2
  277. package/dist/components/questions/types/word-fill-paragraph/register.js +1 -136
  278. package/dist/components/questions/types/word-fill-paragraph/transform.js +1 -56
  279. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +1 -138
  280. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +1 -261
  281. package/dist/components/questions/types/word-fill-structured-form/index.js +1 -2
  282. package/dist/components/questions/types/word-fill-structured-form/register.js +1 -67
  283. package/dist/components/questions/types/word-fill-structured-form/transform.js +1 -40
  284. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +1 -84
  285. package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +1 -192
  286. package/dist/components/questions/types/word-ordering/index.js +1 -2
  287. package/dist/components/questions/types/word-ordering/register.js +1 -46
  288. package/dist/components/questions/types/word-ordering/transform.js +1 -24
  289. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +1 -98
  290. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +1 -320
  291. package/dist/components/questions/types/write-a-short-letter/index.js +1 -2
  292. package/dist/components/questions/types/write-a-short-letter/register.js +1 -68
  293. package/dist/components/questions/types/write-a-short-letter/transform.js +1 -25
  294. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +1 -61
  295. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +1 -196
  296. package/dist/components/questions/types/write-correct-verb-form/index.js +1 -2
  297. package/dist/components/questions/types/write-correct-verb-form/register.js +1 -62
  298. package/dist/components/questions/types/write-correct-verb-form/transform.js +1 -17
  299. package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +1 -22
  300. package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +1 -174
  301. package/dist/components/questions/types/write-sentences/index.js +1 -2
  302. package/dist/components/questions/types/write-sentences/register.js +1 -58
  303. package/dist/components/questions/types/write-sentences/transform.js +1 -28
  304. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +1 -96
  305. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +1 -161
  306. package/dist/components/questions/types/write-short-paragraph/index.js +1 -2
  307. package/dist/components/questions/types/write-short-paragraph/register.js +1 -4
  308. package/dist/components/questions/types/write-short-paragraph/transform.js +1 -21
  309. package/dist/components/questions/viewer/QuestionGroupViewer.js +1 -36
  310. package/dist/components/questions/viewer/QuestionViewer.js +1 -446
  311. package/dist/components/questions/viewer/index.js +1 -2
  312. package/dist/components/results/ResultReviewPageContainer.js +1 -98
  313. package/dist/components/results/ReviewQuestionRenderer.js +1 -72
  314. package/dist/components/results/index.js +1 -2
  315. package/dist/components/results/result-review.utils.js +1 -6
  316. package/dist/components/results/review-data.js +1 -150
  317. package/dist/components/results/review-renderer.utils.js +1 -97
  318. package/dist/components/results/review-types.js +1 -1
  319. package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +1 -138
  320. package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +1 -12
  321. package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +1 -18
  322. package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +1 -11
  323. package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +1 -11
  324. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +1 -9
  325. package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +1 -13
  326. package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +1 -8
  327. package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +1 -6
  328. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +1 -5
  329. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +1 -9
  330. package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +1 -41
  331. package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -25
  332. package/dist/components/themes/cambridge-yle/index.js +1 -13
  333. package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +1 -24
  334. package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +1 -88
  335. package/dist/components/themes/english-certification/EcWordFillParagraph.js +1 -55
  336. package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +1 -156
  337. package/dist/components/themes/english-certification/EcWritingTask.js +1 -34
  338. package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -114
  339. package/dist/components/themes/english-certification/EnglishCertificationBrand.js +1 -11
  340. package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +1 -21
  341. package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +1 -15
  342. package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +1 -19
  343. package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +1 -148
  344. package/dist/components/themes/english-certification/EnglishCertificationHeader.js +1 -31
  345. package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +1 -71
  346. package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +1 -33
  347. package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +1 -16
  348. package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +1 -22
  349. package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +1 -37
  350. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -72
  351. package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +1 -90
  352. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +1 -18
  353. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +1 -29
  354. package/dist/components/themes/english-certification/english-certification-theme.js +1 -58
  355. package/dist/components/themes/english-certification/grouped-question-utils.js +1 -218
  356. package/dist/components/themes/english-certification/index.js +1 -21
  357. package/dist/components/themes/index.js +1 -6
  358. package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +1 -170
  359. package/dist/components/themes/summer-sky/SummerSkyLayout.js +1 -221
  360. package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +1 -18
  361. package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +1 -194
  362. package/dist/components/themes/types.js +1 -1
  363. package/dist/components/ui/alert-dialog.js +1 -26
  364. package/dist/components/ui/alert.js +1 -22
  365. package/dist/components/ui/avatar.js +1 -12
  366. package/dist/components/ui/badge.js +1 -38
  367. package/dist/components/ui/button.js +1 -35
  368. package/dist/components/ui/calendar.js +1 -76
  369. package/dist/components/ui/card.js +1 -16
  370. package/dist/components/ui/carousel.js +1 -91
  371. package/dist/components/ui/checkbox.js +1 -9
  372. package/dist/components/ui/collapsible.js +1 -13
  373. package/dist/components/ui/command.js +1 -34
  374. package/dist/components/ui/confirm-dialog.js +1 -49
  375. package/dist/components/ui/dialog.js +1 -23
  376. package/dist/components/ui/drawer.js +1 -35
  377. package/dist/components/ui/dropdown-menu.js +1 -33
  378. package/dist/components/ui/file-upload.js +1 -155
  379. package/dist/components/ui/form.js +1 -62
  380. package/dist/components/ui/hover-card.js +1 -10
  381. package/dist/components/ui/image-preview.js +1 -29
  382. package/dist/components/ui/input.js +1 -8
  383. package/dist/components/ui/label.js +1 -9
  384. package/dist/components/ui/language-switcher.js +1 -15
  385. package/dist/components/ui/multi-select.js +1 -90
  386. package/dist/components/ui/points-input.js +1 -33
  387. package/dist/components/ui/popover.js +1 -26
  388. package/dist/components/ui/progress.js +1 -16
  389. package/dist/components/ui/radio-group.js +1 -12
  390. package/dist/components/ui/scroll-area.js +1 -11
  391. package/dist/components/ui/select.js +1 -38
  392. package/dist/components/ui/separator.js +1 -8
  393. package/dist/components/ui/sheet.js +1 -40
  394. package/dist/components/ui/single-select.js +1 -76
  395. package/dist/components/ui/skeleton.js +1 -6
  396. package/dist/components/ui/switch.js +1 -8
  397. package/dist/components/ui/table.js +1 -20
  398. package/dist/components/ui/tabs.js +1 -17
  399. package/dist/components/ui/textarea.js +1 -8
  400. package/dist/components/ui/tooltip.js +1 -11
  401. package/dist/index.d.ts +3 -3
  402. package/dist/index.js +1 -19
  403. package/dist/mcp/config.js +1 -32
  404. package/dist/mcp/http-client.js +1 -48
  405. package/dist/mcp/index.js +1 -29
  406. package/dist/mcp/tools/_helpers.js +1 -25
  407. package/dist/mcp/tools/exams.js +1 -439
  408. package/dist/mcp/tools/index.js +1 -6
  409. package/dist/mcp/tools/results.js +1 -19
  410. package/dist/shared/constants/ApiConstant.js +1 -276
  411. package/dist/shared/constants/QueryKeyConstant.js +1 -112
  412. package/dist/shared/constants/exam-level.enum.js +1 -41
  413. package/dist/shared/constants/landings/placement-test.constants.js +1 -431
  414. package/dist/shared/constants/question-skills.js +1 -267
  415. package/dist/shared/constants/routes.js +1 -81
  416. package/dist/shared/index.d.ts +3 -0
  417. package/dist/shared/index.js +1 -7
  418. package/dist/shared/lib/hooks/index.js +1 -10
  419. package/dist/shared/lib/hooks/useAlertDialog.js +1 -57
  420. package/dist/shared/lib/hooks/useAudioPlayCounter.js +1 -30
  421. package/dist/shared/lib/hooks/useAudioPreview.js +1 -81
  422. package/dist/shared/lib/hooks/useAudioUpload.js +1 -24
  423. package/dist/shared/lib/hooks/useConfirmDialog.js +1 -89
  424. package/dist/shared/lib/hooks/useDebouncedCallback.js +1 -32
  425. package/dist/shared/lib/hooks/useDelayedAutoPlay.js +1 -12
  426. package/dist/shared/lib/hooks/useFileUpload.js +1 -24
  427. package/dist/shared/lib/hooks/useImageUrl.js +1 -5
  428. package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -10
  429. package/dist/shared/lib/hooks/useReactHookForm.js +1 -31
  430. package/dist/shared/lib/hooks/useToast.js +1 -62
  431. package/dist/shared/lib/i18n/index.js +1 -54
  432. package/dist/shared/lib/i18n/messages/en/admin.js +1 -2104
  433. package/dist/shared/lib/i18n/messages/en/auth.js +1 -86
  434. package/dist/shared/lib/i18n/messages/en/common.js +1 -140
  435. package/dist/shared/lib/i18n/messages/en/file-upload.js +1 -18
  436. package/dist/shared/lib/i18n/messages/en/student.js +1 -133
  437. package/dist/shared/lib/i18n/messages/en/terms.js +1 -37
  438. package/dist/shared/lib/i18n/messages/en.js +1 -15
  439. package/dist/shared/lib/i18n/messages/vi/admin.js +1 -2104
  440. package/dist/shared/lib/i18n/messages/vi/auth.js +1 -86
  441. package/dist/shared/lib/i18n/messages/vi/common.js +1 -140
  442. package/dist/shared/lib/i18n/messages/vi/file-upload.js +1 -18
  443. package/dist/shared/lib/i18n/messages/vi/student.js +1 -133
  444. package/dist/shared/lib/i18n/messages/vi/terms.js +1 -37
  445. package/dist/shared/lib/i18n/messages/vi.js +1 -15
  446. package/dist/shared/lib/i18n/types.js +1 -1
  447. package/dist/shared/lib/stores/examQuestionStore.js +1 -212
  448. package/dist/shared/lib/stores/localeStore.js +1 -10
  449. package/dist/shared/lib/utils/public-page.js +1 -44
  450. package/dist/shared/lib/utils/question-reverse-transform.d.ts +6 -0
  451. package/dist/shared/lib/utils/question-reverse-transform.js +1 -0
  452. package/dist/shared/lib/utils/question-transform-types.js +1 -1
  453. package/dist/shared/lib/utils/question-transform-validation.d.ts +6 -0
  454. package/dist/shared/lib/utils/question-transform-validation.js +1 -0
  455. package/dist/shared/lib/utils/question-transform.d.ts +6 -2
  456. package/dist/shared/lib/utils/question-transform.js +1 -174
  457. package/dist/shared/lib/utils.js +1 -168
  458. package/dist/shared/lib/validation/choose-correct-answer.validation.js +1 -111
  459. package/dist/shared/lib/validation/label-the-picture.validation.js +1 -19
  460. package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +1 -46
  461. package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +1 -21
  462. package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +1 -34
  463. package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +1 -40
  464. package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +1 -45
  465. package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +1 -19
  466. package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +1 -39
  467. package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +1 -32
  468. package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +1 -19
  469. package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +1 -28
  470. package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +1 -12
  471. package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +1 -47
  472. package/dist/shared/lib/validation/read-and-color-objects.validation.js +1 -29
  473. package/dist/shared/lib/validation/read-displayed-content.validation.js +1 -28
  474. package/dist/shared/lib/validation/speaking-conversation.validation.js +1 -75
  475. package/dist/shared/lib/validation/speaking-describe-image.validation.js +1 -31
  476. package/dist/shared/lib/validation/spontaneous-qa.validation.js +1 -48
  477. package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +1 -38
  478. package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +1 -39
  479. package/dist/shared/lib/validation/word-fill-paragraph.validation.js +1 -43
  480. package/dist/shared/lib/validation/word-ordering.validation.js +1 -33
  481. package/dist/shared/types/branch.types.js +1 -6
  482. package/dist/shared/types/common.types.js +1 -50
  483. package/dist/shared/types/entities/exam-schedule.types.js +1 -5
  484. package/dist/shared/types/entities/exam.types.js +1 -1
  485. package/dist/shared/types/entities/question.types.js +1 -4
  486. package/dist/shared/types/entities/result.types.js +1 -4
  487. package/dist/shared/types/entities/user.types.js +1 -4
  488. package/dist/shared/types/exam-taking.types.js +1 -2
  489. package/dist/shared/types/history.types.js +1 -34
  490. package/dist/shared/types/index.js +1 -1
  491. package/dist/shared/types/questions/choose-correct-adjective.js +1 -4
  492. package/dist/shared/types/questions/choose-the-correct-answer.js +1 -9
  493. package/dist/shared/types/questions/fill-missing-words-in-grid.js +1 -11
  494. package/dist/shared/types/questions/index.js +1 -40
  495. package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
  496. package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
  497. package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
  498. package/dist/shared/types/questions/listen-and-speak-info-exchange.js +1 -3
  499. package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +1 -3
  500. package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
  501. package/dist/shared/types/questions/listen-and-speak-with-story-images.js +1 -3
  502. package/dist/shared/types/questions/look-picture-choose-correct-answer.js +1 -5
  503. package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +1 -9
  504. package/dist/shared/types/questions/look-picture-fill-word-hint.js +1 -7
  505. package/dist/shared/types/questions/read-displayed-content.js +1 -1
  506. package/dist/shared/types/questions/speaking-conversation.js +1 -7
  507. package/dist/shared/types/questions/speaking-describe-image.js +1 -1
  508. package/dist/shared/types/questions/spontaneous-qa.js +1 -1
  509. package/dist/shared/types/questions/write-correct-verb-form.js +1 -12
  510. package/dist/shared/types/role.types.js +1 -1
  511. package/dist/shared/types/structure-management.js +1 -28
  512. package/dist/shared/types/student.types.js +1 -54
  513. package/dist/shared/types/teacher-dashboard-rooms.js +1 -2
  514. package/package.json +12 -3
@@ -1,8 +1,11 @@
1
1
  import { type UseFormReturn } from 'react-hook-form';
2
2
  import type { ExamFormData, ExamLevelOption } from '../../api/exam-create';
3
+ import type { CreateExamTexts } from './create-exam.texts';
3
4
  export interface CreateExamInfoFormProps {
4
5
  form: UseFormReturn<ExamFormData>;
5
6
  examLevels: ExamLevelOption[];
6
7
  isLoadingExamLevels?: boolean;
8
+ /** Optional label overrides (e.g. to reuse this form for homework). */
9
+ texts?: CreateExamTexts;
7
10
  }
8
- export declare function CreateExamInfoForm({ form, examLevels, isLoadingExamLevels }: CreateExamInfoFormProps): import("react").JSX.Element;
11
+ export declare function CreateExamInfoForm({ form, examLevels, isLoadingExamLevels, texts, }: CreateExamInfoFormProps): import("react").JSX.Element;
@@ -1,35 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useRef } from 'react';
4
- import { Controller } from 'react-hook-form';
5
- import { FileText, Loader2 } from 'lucide-react';
6
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
7
- import { Input } from '../../components/ui/input';
8
- import { Label } from '../../components/ui/label';
9
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
10
- import { Textarea } from '../../components/ui/textarea';
11
- import { useT } from '../../shared/lib/i18n';
12
- export function CreateExamInfoForm({ form, examLevels, isLoadingExamLevels = false }) {
13
- const t = useT();
14
- const isCodeManuallyEdited = useRef(false);
15
- const nameValue = form.watch('name');
16
- const setValueRef = useRef(form.setValue);
17
- setValueRef.current = form.setValue;
18
- const generateCodeFromName = useCallback((name) => name.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/đ/g, 'd').replace(/Đ/g, 'D').toLowerCase().trim().replace(/[^a-z0-9\s-]/g, '').replace(/[\s]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 100), []);
19
- useEffect(() => {
20
- if (isCodeManuallyEdited.current)
21
- return;
22
- setValueRef.current('code', nameValue ? generateCodeFromName(nameValue) : '', { shouldValidate: false });
23
- }, [nameValue, generateCodeFromName]);
24
- const hasError = (field) => !!form.formState.errors[field];
25
- const error = (field) => form.formState.errors[field]?.message;
26
- return (_jsxs(Card, { className: "border border-gray-200", children: [_jsxs(CardHeader, { className: "border-b border-gray-100 bg-gray-50/50", children: [_jsxs(CardTitle, { className: "flex items-center gap-2 text-base font-semibold", children: [_jsx(FileText, { className: "h-4 w-4 text-blue-600" }), t('admin.exams.form.basicInfo')] }), _jsx(CardDescription, { children: t('admin.exams.form.requiredHint') })] }), _jsxs(CardContent, { className: "space-y-4 pt-6", children: [_jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsx(Field, { label: t('admin.exams.form.nameLabel'), error: error('name'), required: true, children: _jsx(Input, { id: "name", maxLength: 255, className: hasError('name') ? 'border-red-500' : '', placeholder: t('admin.exams.form.namePlaceholder'), ...form.register('name') }) }), _jsx(Field, { label: t('admin.exams.form.codeLabel'), error: error('code'), required: true, children: _jsx(CodeInput, { form: form, hasError: hasError('code'), onManualEdit: (value) => { isCodeManuallyEdited.current = value.length > 0; } }) }), _jsx(Field, { label: t('admin.exams.form.totalPoints'), error: error('totalPoints'), required: true, children: _jsx(Controller, { name: "totalPoints", control: form.control, render: ({ field }) => _jsx(Input, { id: "totalPoints", inputMode: "numeric", value: field.value, onBlur: field.onBlur, onChange: (event) => field.onChange(event.target.value.replace(/[^0-9]/g, '')), className: hasError('totalPoints') ? 'border-red-500' : '' }) }) }), _jsx(Field, { label: t('admin.exams.form.level'), error: error('level'), children: _jsx(Controller, { name: "level", control: form.control, render: ({ field }) => _jsxs(Select, { value: field.value, onValueChange: field.onChange, disabled: isLoadingExamLevels, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: isLoadingExamLevels ? t('admin.exams.form.loading') : t('admin.exams.form.levelPlaceholder') }) }), _jsx(SelectContent, { children: isLoadingExamLevels ? _jsx("div", { className: "flex justify-center p-4", children: _jsx(Loader2, { className: "h-4 w-4 animate-spin" }) }) : examLevels.map((level) => _jsx(SelectItem, { value: level.id, children: level.name }, level.id)) })] }) }) }), _jsx(Field, { label: t('admin.exams.form.difficulty'), error: error('difficulty'), children: _jsx(Controller, { name: "difficulty", control: form.control, render: ({ field }) => _jsxs(Select, { value: field.value, onValueChange: field.onChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: t('admin.exams.form.difficultyPlaceholder') }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "easy", children: t('admin.exams.difficulty.easy') }), _jsx(SelectItem, { value: "medium", children: t('admin.exams.difficulty.medium') }), _jsx(SelectItem, { value: "hard", children: t('admin.exams.difficulty.hard') })] })] }) }) }), _jsx(Field, { label: t('admin.exams.form.duration'), error: error('duration'), children: _jsx(Input, { id: "duration", inputMode: "numeric", placeholder: "60", className: hasError('duration') ? 'border-red-500' : '', ...form.register('duration') }) })] }), _jsx(Field, { label: t('admin.exams.form.description'), error: error('description'), children: _jsx(Textarea, { id: "description", rows: 4, maxLength: 2000, placeholder: t('admin.exams.form.descriptionPlaceholder'), ...form.register('description') }) })] })] }));
27
- }
28
- function CodeInput({ form, hasError, onManualEdit }) {
29
- const t = useT();
30
- const { onChange, ...register } = form.register('code');
31
- return _jsx(Input, { id: "code", maxLength: 100, className: hasError ? 'border-red-500' : '', placeholder: t('admin.exams.form.codePlaceholder'), ...register, onChange: (event) => { onChange(event); onManualEdit(event.target.value); } });
32
- }
33
- function Field({ label, error, required, children }) {
34
- return _jsxs("div", { children: [_jsxs(Label, { children: [label, " ", required && _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx("div", { className: "mt-1", children: children }), error && _jsx("p", { className: "mt-1 text-sm text-red-600", children: error })] });
35
- }
1
+ "use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useCallback as a,useEffect as n,useRef as o}from"react";import{Controller as l}from"react-hook-form";import{FileText as i,Loader2 as t}from"lucide-react";import{Card as d,CardContent as c,CardDescription as m,CardHeader as s,CardTitle as u}from"../../components/ui/card";import{Input as h}from"../../components/ui/input";import{Label as f}from"../../components/ui/label";import{Select as p,SelectContent as g,SelectItem as x,SelectTrigger as b,SelectValue as N}from"../../components/ui/select";import{Textarea as v}from"../../components/ui/textarea";import{useT as y}from"../../shared/lib/i18n";const C=["EASY","MEDIUM","HARD"];export function CreateExamInfoForm({form:f,examLevels:S,isLoadingExamLevels:w=!1,texts:M}){const q=y(),D=o(!1),I=f.watch("name"),V=o(f.setValue);V.current=f.setValue;const j=a(e=>e.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/đ/g,"d").replace(/Đ/g,"D").toLowerCase().trim().replace(/[^a-z0-9\s-]/g,"").replace(/[\s]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").substring(0,100),[]);n(()=>{D.current||V.current("code",I?j(I):"",{shouldValidate:!1})},[I,j]);const A=e=>Boolean(f.formState.errors[e]),H=e=>f.formState.errors[e]?.message;return r(d,{className:"border border-gray-200",children:[r(s,{className:"border-b border-gray-100 bg-gray-50/50",children:[r(u,{className:"flex items-center gap-2 text-base font-semibold",children:[e(i,{className:"h-4 w-4 text-blue-600"}),M?.basicInfo??q("admin.exams.form.basicInfo")]}),e(m,{children:M?.requiredHint??q("admin.exams.form.requiredHint")})]}),e(c,{className:"space-y-4 pt-6",children:r("div",{className:"grid grid-cols-1 gap-4 md:grid-cols-2",children:[e(P,{label:M?.nameLabel??q("admin.exams.form.nameLabel"),error:H("name"),required:!0,hint:q("admin.exams.form.charCount",{count:String(I?.length||0),max:String(255)}),children:e(h,{id:"name",maxLength:255,className:A("name")?"border-red-500 focus:ring-red-500":"",placeholder:M?.namePlaceholder??q("admin.exams.form.namePlaceholder"),...f.register("name")})}),e(P,{label:M?.codeLabel??q("admin.exams.form.codeLabel"),error:H("code"),required:!0,hint:q("admin.exams.form.charCount",{count:String(f.watch("code")?.length||0),max:String(100)}),children:e(L,{form:f,hasError:A("code"),placeholder:M?.codePlaceholder??q("admin.exams.form.codePlaceholder"),onManualEdit:e=>{D.current=e.length>0}})}),e(P,{label:q("admin.exams.form.totalPoints"),error:H("totalPoints"),required:!0,children:e(l,{name:"totalPoints",control:f.control,render:({field:r})=>e(h,{id:"totalPoints",type:"text",inputMode:"numeric",placeholder:"100",value:r.value,onBlur:r.onBlur,onChange:e=>r.onChange(e.target.value.replace(/[^0-9]/g,"")),className:A("totalPoints")?"border-red-500 focus:ring-red-500":""})})}),e(P,{label:q("admin.exams.form.level"),error:H("level"),children:e(l,{name:"level",control:f.control,render:({field:a})=>r(p,{value:a.value,onValueChange:a.onChange,disabled:w,children:[e(b,{className:A("level")?"border-red-500 focus:ring-red-500":"",children:e(N,{placeholder:q(w?"admin.exams.form.loading":"admin.exams.form.levelPlaceholder")})}),e(g,{children:w?e("div",{className:"flex items-center justify-center p-4",children:e(t,{className:"h-4 w-4 animate-spin text-gray-400"})}):S.map(r=>e(x,{value:r.id,children:r.name},r.id))})]})})}),e(P,{label:q("admin.exams.form.difficulty"),error:H("difficulty"),children:e(l,{name:"difficulty",control:f.control,render:({field:a})=>r(p,{value:a.value,onValueChange:a.onChange,children:[e(b,{className:A("difficulty")?"border-red-500 focus:ring-red-500":"",children:e(N,{placeholder:q("admin.exams.form.difficultyPlaceholder")})}),e(g,{children:C.map(r=>e(x,{value:r,children:E(r,q)},r))})]})})}),e(P,{label:M?.durationLabel??q("admin.exams.form.duration"),error:H("duration"),children:e(h,{id:"duration",type:"number",inputMode:"numeric",min:5,max:300,placeholder:"45",className:A("duration")?"border-red-500 focus:ring-red-500":"",...f.register("duration")})}),e(P,{className:"md:col-span-2",label:M?.descriptionLabel??q("admin.exams.form.description"),error:H("description"),hint:q("admin.exams.form.charCount",{count:String(f.watch("description")?.length||0),max:String(2e3)}),children:e(v,{id:"description",rows:3,maxLength:2e3,placeholder:M?.descriptionPlaceholder??q("admin.exams.form.descriptionPlaceholder"),className:A("description")?"border-red-500 focus:ring-red-500":"",...f.register("description")})})]})})]})}function L({form:r,hasError:a,placeholder:n,onManualEdit:o}){const{onChange:l,...i}=r.register("code");return e(h,{id:"code",maxLength:100,className:a?"border-red-500 focus:ring-red-500":"",placeholder:n,...i,onChange:e=>{l(e),o(e.target.value)}})}function P({label:a,error:n,hint:o,required:l,className:i,children:t}){return r("div",{className:i,children:[r(f,{children:[a," ",l&&e("span",{className:"text-red-500",children:"*"})]}),e("div",{className:"mt-1",children:t}),n&&e("p",{className:"mt-1 text-sm text-red-600",children:n}),o&&e("p",{className:"mt-1 text-xs text-gray-500",children:o})]})}function E(e,r){switch(e){case"EASY":return r("admin.exams.form.easy");case"MEDIUM":return r("admin.exams.form.medium");case"HARD":return r("admin.exams.form.hard");default:return e}}
@@ -1,9 +1,12 @@
1
1
  import { UserRole } from '../../shared/types/common.types';
2
2
  import type { CreateExamSdkApi } from '../../api/exam-create';
3
+ import type { CreateExamTexts } from './create-exam.texts';
3
4
  export interface CreateExamPageContainerProps {
4
5
  role?: UserRole.ADMIN | UserRole.TEACHER;
5
6
  api: CreateExamSdkApi;
6
7
  listPath?: string;
7
8
  onNavigate?: (href: string) => void;
9
+ /** Optional label overrides (e.g. to reuse this flow for homework). */
10
+ texts?: CreateExamTexts;
8
11
  }
9
12
  export declare function CreateExamPageContainer(props: CreateExamPageContainerProps): import("react").JSX.Element;
@@ -1,103 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { Suspense, useEffect, useState } from 'react';
4
- import { zodResolver } from '@hookform/resolvers/zod';
5
- import { useForm } from 'react-hook-form';
6
- import { ArrowLeft, ArrowRight, LayoutTemplate, Loader2, X } from 'lucide-react';
7
- import { Button } from '../../components/ui/button';
8
- import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
9
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../components/ui/dialog';
10
- import { useToast } from '../../shared/lib/hooks/useToast';
11
- import { useT } from '../../shared/lib/i18n';
12
- import { buildExamTemplatePayload } from '../../api/exam-create';
13
- import { CreateExamInfoForm } from './CreateExamInfoForm';
14
- import { TemplateSelector } from './TemplateSelector';
15
- import { examFormSchema } from './create-exam.validation';
16
- function CreateExamContent({ api, listPath = '/manage/exams', onNavigate }) {
17
- const t = useT();
18
- const { success, warning, error: errorToast } = useToast();
19
- const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false);
20
- const [selectedTemplate, setSelectedTemplate] = useState(null);
21
- const [examLevels, setExamLevels] = useState([]);
22
- const [isLoadingExamLevels, setIsLoadingExamLevels] = useState(false);
23
- const [isSaving, setIsSaving] = useState(false);
24
- const form = useForm({
25
- resolver: zodResolver(examFormSchema),
26
- mode: 'onBlur',
27
- defaultValues: { code: '', name: '', level: '', difficulty: '', duration: '', totalPoints: '100', description: '' },
28
- });
29
- useEffect(() => {
30
- if (!api.getExamLevels)
31
- return;
32
- let ignore = false;
33
- setIsLoadingExamLevels(true);
34
- api.getExamLevels()
35
- .then((levels) => { if (!ignore)
36
- setExamLevels(levels); })
37
- .catch((err) => errorToast({ message: t('admin.exams.questions.error'), description: err instanceof Error ? err.message : undefined }))
38
- .finally(() => { if (!ignore)
39
- setIsLoadingExamLevels(false); });
40
- return () => { ignore = true; };
41
- }, [api, errorToast, t]);
42
- const navigate = (href) => {
43
- if (onNavigate)
44
- onNavigate(href);
45
- else if (typeof window !== 'undefined')
46
- window.location.href = href;
47
- };
48
- const handleSubmit = form.handleSubmit(async (data) => {
49
- if (!selectedTemplate) {
50
- warning({ message: t('admin.exams.create.incompleteInfo'), description: 'Vui lòng chọn template đề thi trước khi tiếp tục.' });
51
- return;
52
- }
53
- setIsSaving(true);
54
- try {
55
- const exam = unwrap(await api.createExam({
56
- name: data.name,
57
- code: data.code || undefined,
58
- examLevel: data.level || undefined,
59
- difficulty: toDifficulty(data.difficulty),
60
- totalScore: Number.parseInt(data.totalPoints, 10),
61
- durationMinutes: data.duration ? Number.parseInt(data.duration, 10) : undefined,
62
- description: data.description || undefined,
63
- isComplete: false,
64
- isActive: false,
65
- examTemplateId: selectedTemplate.id,
66
- }));
67
- await api.updateExamTemplate({ examId: exam.id, template: buildExamTemplatePayload(selectedTemplate) });
68
- success({ message: t('admin.exams.create.success'), description: t('admin.exams.create.successDesc', { name: exam.name }) });
69
- navigate(`${listPath}/${exam.id}/questions`);
70
- }
71
- catch (err) {
72
- errorToast({ message: t('admin.exams.questions.error'), description: err instanceof Error ? err.message : undefined });
73
- }
74
- finally {
75
- setIsSaving(false);
76
- }
77
- }, () => warning({ message: t('admin.exams.create.incompleteInfo'), description: t('admin.exams.create.incompleteInfoDesc') }));
78
- return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx(Button, { variant: "ghost", size: "icon", onClick: () => navigate(listPath), className: "h-8 w-8", children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold tracking-tight text-gray-900", children: t('admin.exams.create.title') }), _jsx("p", { className: "mt-1 text-gray-600", children: t('admin.exams.create.step1') })] })] }), _jsx("form", { id: "exam-form", onSubmit: handleSubmit, children: _jsx(CreateExamInfoForm, { form: form, examLevels: examLevels, isLoadingExamLevels: isLoadingExamLevels }) }), _jsx(TemplateSection, { selectedTemplate: selectedTemplate, onOpen: () => setIsTemplateDialogOpen(true), onClear: () => setSelectedTemplate(null) }), _jsx(Dialog, { open: isTemplateDialogOpen, onOpenChange: setIsTemplateDialogOpen, children: _jsxs(DialogContent, { className: "flex max-h-[92vh] min-h-[92vh] max-w-5xl flex-col overflow-hidden", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Ch\u1ECDn template \u0111\u1EC1 thi" }), _jsx(DialogDescription, { children: "Ch\u1ECDn lo\u1EA1i \u0111\u1EC1 thi b\u1EA1n mu\u1ED1n t\u1EA1o." })] }), _jsx(TemplateSelector, { api: api, onTemplateSelect: (template) => { setSelectedTemplate(template); setIsTemplateDialogOpen(false); }, onCancel: () => setIsTemplateDialogOpen(false) })] }) }), _jsxs("div", { className: "flex justify-end gap-3", children: [_jsx(Button, { variant: "outline", onClick: () => navigate(listPath), disabled: isSaving, children: t('admin.exams.create.cancel') }), _jsx(Button, { type: "submit", form: "exam-form", className: "bg-blue-600 hover:bg-blue-700", disabled: isSaving || !selectedTemplate, children: isSaving ? _jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), t('admin.exams.create.saving')] }) : _jsxs(_Fragment, { children: [t('admin.exams.create.saveAndContinue'), _jsx(ArrowRight, { className: "ml-2 h-4 w-4" })] }) })] })] }));
79
- }
80
- export function CreateExamPageContainer(props) {
81
- return _jsx(Suspense, { fallback: _jsx(Loading, {}), children: _jsx(CreateExamContent, { ...props }) });
82
- }
83
- function TemplateSection({ selectedTemplate, onOpen, onClear }) {
84
- return _jsxs(Card, { className: "border border-gray-200", children: [_jsx(CardHeader, { className: "border-b border-gray-100 bg-gray-50/50", children: _jsxs(CardTitle, { className: "flex items-center gap-2 text-base font-semibold", children: [_jsx(LayoutTemplate, { className: "h-4 w-4 text-blue-600" }), "Ch\u1ECDn Template ", _jsx("span", { className: "text-red-500", children: "*" })] }) }), _jsx(CardContent, { className: "pt-6", children: selectedTemplate ? _jsxs("div", { className: "flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-green-800", children: selectedTemplate.name }), _jsxs("p", { className: "text-xs text-green-600", children: [selectedTemplate.totalQuestions, " c\u00E2u h\u1ECFi \u2022 ", selectedTemplate.parts.length, " ph\u1EA7n"] })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onOpen, children: "\u0110\u1ED5i template" }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: onClear, children: _jsx(X, { className: "h-3.5 w-3.5" }) })] })] }) : _jsxs("div", { className: "flex flex-col items-center gap-3 rounded-lg border-2 border-dashed border-gray-300 py-8", children: [_jsx(LayoutTemplate, { className: "h-8 w-8 text-gray-400" }), _jsx("p", { className: "text-sm text-gray-500", children: "Ch\u01B0a ch\u1ECDn template \u0111\u1EC1 thi" }), _jsxs(Button, { type: "button", variant: "outline", onClick: onOpen, children: [_jsx(LayoutTemplate, { className: "mr-2 h-4 w-4" }), "Ch\u1ECDn template"] })] }) })] });
85
- }
86
- function Loading() {
87
- return _jsx("div", { className: "flex h-screen items-center justify-center", children: _jsx(Loader2, { className: "h-8 w-8 animate-spin text-blue-600" }) });
88
- }
89
- function toDifficulty(value) {
90
- if (value === 'easy')
91
- return 'EASY';
92
- if (value === 'medium')
93
- return 'MEDIUM';
94
- if (value === 'hard')
95
- return 'HARD';
96
- return undefined;
97
- }
98
- function unwrap(value) {
99
- return hasData(value) ? value.data : value;
100
- }
101
- function hasData(value) {
102
- return typeof value === 'object' && value !== null && 'data' in value;
103
- }
1
+ "use client";import{jsx as e,jsxs as t,Fragment as a}from"react/jsx-runtime";import{Suspense as n,useEffect as i,useState as r}from"react";import{zodResolver as s}from"@hookform/resolvers/zod";import{useForm as l}from"react-hook-form";import{ArrowLeft as o,ArrowRight as m,LayoutTemplate as c,Loader2 as d,X as p}from"lucide-react";import{Button as h}from"../../components/ui/button";import{Card as u,CardContent as x,CardHeader as f,CardTitle as g}from"../../components/ui/card";import{Dialog as b,DialogContent as v,DialogDescription as N,DialogHeader as y,DialogTitle as w}from"../../components/ui/dialog";import{useToast as C}from"../../shared/lib/hooks/useToast";import{useT as T}from"../../shared/lib/i18n";import{buildExamTemplatePayload as E}from"../../api/exam-create";import{CreateExamInfoForm as k}from"./CreateExamInfoForm";import{TemplateSelector as D}from"./TemplateSelector";import{examFormSchema as L}from"./create-exam.validation";function I({api:n,listPath:c="/manage/exams",onNavigate:p,texts:u}){const x=T(),{success:f,warning:g,error:I}=C(),[S,M]=r(!1),[z,P]=r(null),[q,O]=r([]),[F,H]=r(!1),[R,U]=r(!1),V=l({resolver:s(L),mode:"onBlur",defaultValues:{code:"",name:"",level:"",difficulty:"",duration:"",totalPoints:"100",description:""}});i(()=>{if(!n.getExamLevels)return;let e=!1;return H(!0),n.getExamLevels().then(t=>{e||O(t)}).catch(e=>I({message:x("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})).finally(()=>{e||H(!1)}),()=>{e=!0}},[n,I,x]);const Y=e=>{p?p(e):"undefined"!=typeof window&&(window.location.href=e)},$=V.handleSubmit(async e=>{if(z){U(!0);try{const a=function(e){return"object"==typeof e&&null!==e&&"data"in e}(t=await n.createExam({name:e.name,code:e.code||void 0,examLevel:e.level||void 0,difficulty:A(e.difficulty),totalScore:Number.parseInt(e.totalPoints,10),durationMinutes:e.duration?Number.parseInt(e.duration,10):void 0,description:e.description||void 0,isComplete:!1,isActive:!1,examTemplateId:z.id}))?t.data:t;await n.updateExamTemplate({examId:a.id,template:E(z)}),f({message:x("admin.exams.create.success"),description:u?.successDescription?.replace("{name}",a.name)??x("admin.exams.create.successDesc",{name:a.name})}),Y(`${c}/${a.id}/questions`)}catch(e){I({message:x("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})}finally{U(!1)}var t}else g({message:x("admin.exams.create.incompleteInfo"),description:u?.missingTemplateDescription??"Vui lòng chọn template đề thi trước khi tiếp tục."})},()=>g({message:x("admin.exams.create.incompleteInfo"),description:x("admin.exams.create.incompleteInfoDesc")}));return t("div",{className:"space-y-6",children:[t("div",{className:"flex items-center gap-4",children:[e(h,{variant:"ghost",size:"icon",onClick:()=>Y(c),className:"h-8 w-8",children:e(o,{className:"h-4 w-4"})}),t("div",{children:[e("h1",{className:"text-3xl font-bold tracking-tight text-gray-900",children:u?.title??x("admin.exams.create.title")}),e("p",{className:"mt-1 text-gray-600",children:u?.step1??x("admin.exams.create.step1")})]})]}),e("form",{id:"exam-form",onSubmit:$,children:e(k,{form:V,examLevels:q,isLoadingExamLevels:F,texts:u})}),e(j,{selectedTemplate:z,onOpen:()=>M(!0),onClear:()=>P(null),texts:u}),e(b,{open:S,onOpenChange:M,children:t(v,{className:"flex max-h-[92vh] min-h-[92vh] max-w-5xl flex-col overflow-hidden",children:[t(y,{children:[e(w,{children:u?.templateDialogTitle??"Chọn template đề thi"}),e(N,{children:u?.templateDialogDescription??"Chọn loại đề thi bạn muốn tạo."})]}),e(D,{api:n,onTemplateSelect:e=>{P(e),M(!1)},onCancel:()=>M(!1)})]})}),t("div",{className:"flex justify-end gap-3",children:[e(h,{variant:"outline",onClick:()=>Y(c),disabled:R,children:u?.cancelLabel??x("admin.exams.create.cancel")}),e(h,{type:"submit",form:"exam-form",className:"bg-blue-600 hover:bg-blue-700",disabled:R||!z,children:t(a,R?{children:[e(d,{className:"mr-2 h-4 w-4 animate-spin"}),u?.savingLabel??x("admin.exams.create.saving")]}:{children:[u?.saveAndContinueLabel??x("admin.exams.create.saveAndContinue"),e(m,{className:"ml-2 h-4 w-4"})]})})]})]})}export function CreateExamPageContainer(t){return e(n,{fallback:e(S,{}),children:e(I,{...t})})}function j({selectedTemplate:a,onOpen:n,onClear:i,texts:r}){return t(u,{className:"border border-gray-200",children:[e(f,{className:"border-b border-gray-100 bg-gray-50/50",children:t(g,{className:"flex items-center gap-2 text-base font-semibold",children:[e(c,{className:"h-4 w-4 text-blue-600"}),r?.templateSectionTitle??"Chọn Template"," ",e("span",{className:"text-red-500",children:"*"})]})}),e(x,{className:"pt-6",children:t("div",a?{className:"flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-green-100",children:e(c,{className:"h-4 w-4 text-green-600"})}),t("div",{children:[e("p",{className:"text-sm font-medium text-green-800",children:a.name}),t("p",{className:"text-xs text-green-600",children:[a.totalQuestions," câu hỏi • ",a.parts.length," phần"]})]})]}),t("div",{className:"flex items-center gap-2",children:[e(h,{type:"button",variant:"outline",size:"sm",onClick:n,className:"border-green-300 text-green-700 hover:bg-green-100",children:r?.changeTemplateLabel??"Đổi template"}),e(h,{type:"button",variant:"ghost",size:"icon",onClick:i,className:"h-7 w-7 text-green-600 hover:bg-green-100 hover:text-green-800",children:e(p,{className:"h-3.5 w-3.5"})})]})]}:{className:"flex flex-col items-center gap-3 rounded-lg border-2 border-dashed border-gray-300 py-8",children:[e(c,{className:"h-8 w-8 text-gray-400"}),e("p",{className:"text-sm text-gray-500",children:r?.noTemplateText??"Chưa chọn template đề thi"}),t(h,{type:"button",variant:"outline",onClick:n,children:[e(c,{className:"mr-2 h-4 w-4"}),r?.selectTemplateLabel??"Chọn template"]})]})})]})}function S(){return e("div",{className:"flex h-screen items-center justify-center",children:e(d,{className:"h-8 w-8 animate-spin text-blue-600"})})}function A(e){return"EASY"===e?"EASY":"MEDIUM"===e?"MEDIUM":"HARD"===e?"HARD":void 0}
@@ -1,5 +1,6 @@
1
+ import { type ExistingQuestionBank } from '../questions/components/QuestionBankOption';
1
2
  import type { ExamData, ExamQuestion, ExamTemplate } from '../../shared/types/entities/exam.types';
2
- import type { SaveQuestionInput } from '../../api/exam-questions.types';
3
+ import type { SaveQuestionInput, SearchQuestionBankInput, SearchQuestionBankResult } from '../../api/exam-questions.types';
3
4
  export interface ExamCreatorProps {
4
5
  template: ExamTemplate;
5
6
  examId?: string;
@@ -7,10 +8,15 @@ export interface ExamCreatorProps {
7
8
  examLevel?: string;
8
9
  subjectId?: string;
9
10
  initialQuestions?: Partial<ExamQuestion>[];
10
- onSaveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | void>;
11
+ onSaveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | {
12
+ question: Partial<ExamQuestion>;
13
+ questionBank?: ExistingQuestionBank | null;
14
+ } | void>;
11
15
  onSave?: (examData: ExamData) => void | Promise<void>;
12
16
  onCancel?: () => void;
13
17
  onPartAudioChange?: (partId: string, audioUrl: string) => void | Promise<void>;
14
18
  partAudioOverrides?: Record<string, string>;
19
+ searchQuestions?: (input: SearchQuestionBankInput) => Promise<SearchQuestionBankResult>;
20
+ saveExamRef?: React.MutableRefObject<(() => void) | null>;
15
21
  }
16
- export declare function ExamCreator({ template, examId, examDifficulty, examLevel, subjectId, initialQuestions, onSaveQuestion, onSave, onCancel, onPartAudioChange, partAudioOverrides }: ExamCreatorProps): import("react").JSX.Element;
22
+ export declare function ExamCreator(props: ExamCreatorProps): import("react").JSX.Element;
@@ -1,89 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useMemo, useRef, useState } from 'react';
4
- import { ChevronLeft, ChevronRight, Save } from 'lucide-react';
5
- import { Button } from '../../components/ui/button';
6
- import { QuestionCreator } from '../questions/creator/QuestionCreator';
7
- import { useToast } from '../../shared/lib/hooks/useToast';
8
- import { useT } from '../../shared/lib/i18n';
9
- import { ExamPartTabs } from './ExamPartTabs';
10
- import { ExamQuestionGrid } from './ExamQuestionGrid';
11
- import { PartAudioUploader } from './PartAudioUploader';
12
- import { getEffectiveQuestionConfig, getPartByQuestionIndex, normalizeTemplateGroupConfig } from './exam-template.utils';
13
- export function ExamCreator({ template, examId, examDifficulty, examLevel, subjectId, initialQuestions = [], onSaveQuestion, onSave, onCancel, onPartAudioChange, partAudioOverrides }) {
14
- const t = useT();
15
- const { success, warning, error } = useToast();
16
- const validationRef = useRef(null);
17
- const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
18
- const [questions, setQuestions] = useState(() => Object.fromEntries(initialQuestions.map((q) => [(q.questionNumber ?? 1) - 1, q])));
19
- const [completedQuestions, setCompletedQuestions] = useState(() => new Set(initialQuestions.map((q) => (q.questionNumber ?? 1) - 1)));
20
- const [validationErrors, setValidationErrors] = useState({});
21
- const [isSavingQuestion, setIsSavingQuestion] = useState(false);
22
- const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
23
- const draftRef = useRef({});
24
- const currentPart = getPartByQuestionIndex(template, currentQuestionIndex);
25
- const normalizedPart = currentPart ? normalizeTemplateGroupConfig(currentPart) : undefined;
26
- const currentConfig = currentPart ? getEffectiveQuestionConfig(currentPart, currentQuestionIndex) : undefined;
27
- const isPartLevelGroup = !!(normalizedPart?.groups && !currentConfig?.groups);
28
- const sectionName = currentConfig && 'sectionName' in currentConfig ? currentConfig.sectionName : undefined;
29
- const sharedDataPartId = sectionName && !isPartLevelGroup ? `${currentPart?.id}_${sectionName}` : currentPart?.id;
30
- const currentQuestion = questions[currentQuestionIndex];
31
- const examData = useMemo(() => ({
32
- examId,
33
- templateId: template.id,
34
- questions: Object.values(questions),
35
- currentQuestionIndex,
36
- lastSaved: new Date().toISOString(),
37
- createdAt: new Date().toISOString(),
38
- updatedAt: new Date().toISOString(),
39
- isCompleted: completedQuestions.size >= template.totalQuestions,
40
- }), [completedQuestions.size, currentQuestionIndex, examId, questions, template.id, template.totalQuestions]);
41
- const navigateToQuestion = (index) => {
42
- if (hasUnsavedChanges)
43
- warning({ message: t('common.examCreator.saveBeforeExit') });
44
- setCurrentQuestionIndex(Math.max(0, Math.min(template.totalQuestions - 1, index)));
45
- setHasUnsavedChanges(false);
46
- draftRef.current = {};
47
- };
48
- const handleSaveQuestion = async (questionData) => {
49
- if (!currentPart)
50
- return;
51
- const valid = await validationRef.current?.triggerValidation?.();
52
- if (valid === false)
53
- return;
54
- const question = {
55
- ...currentQuestion,
56
- ...draftRef.current,
57
- ...questionData,
58
- type: questionData?.type ?? draftRef.current.type ?? currentQuestion?.type ?? currentConfig?.questionType,
59
- partId: currentPart.id,
60
- questionNumber: currentQuestionIndex + 1,
61
- difficulty: (questionData?.difficulty ?? examDifficulty ?? currentQuestion?.difficulty),
62
- skill: currentPart.skill,
63
- isCompleted: true,
64
- };
65
- setIsSavingQuestion(true);
66
- try {
67
- const saved = await onSaveQuestion?.({ question, questionIndex: currentQuestionIndex, template, examId, examDifficulty, examLevel, subjectId });
68
- const nextQuestion = (saved || question);
69
- setQuestions((prev) => ({ ...prev, [currentQuestionIndex]: nextQuestion }));
70
- setCompletedQuestions((prev) => new Set(prev).add(currentQuestionIndex));
71
- setHasUnsavedChanges(false);
72
- success({ message: t('common.examCreator.saveSuccess', { index: String(currentQuestionIndex + 1) }) });
73
- }
74
- catch (err) {
75
- error({ message: t('common.examCreator.saveError', { index: String(currentQuestionIndex + 1) }), description: err instanceof Error ? err.message : undefined });
76
- }
77
- finally {
78
- setIsSavingQuestion(false);
79
- }
80
- };
81
- const handleSaveExam = async () => {
82
- if (hasUnsavedChanges) {
83
- warning({ message: t('common.examCreator.saveBeforeExit') });
84
- return;
85
- }
86
- await onSave?.(examData);
87
- };
88
- return (_jsx("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50/30", children: _jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "w-80 overflow-y-auto border-r border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm", children: _jsx(ExamPartTabs, { template: template, currentPartId: currentPart?.id ?? '', completedQuestions: completedQuestions, onPartChange: (partId) => navigateToQuestion(template.parts.find((part) => part.id === partId)?.startIndex ?? 0) }) }), _jsxs("div", { className: "flex flex-1 flex-col overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-4 pb-4", children: _jsxs("div", { className: "mx-auto max-w-4xl", children: [currentPart?.skill === 'LISTENING' && _jsx(PartAudioUploader, { partId: currentPart.id, partName: currentPart.name, audioUrl: partAudioOverrides?.[currentPart.id] ?? currentPart.audioUrl ?? '', examId: examId, onAudioChange: (url) => void onPartAudioChange?.(currentPart.id, url), isReadOnly: !onPartAudioChange }), _jsx(QuestionCreator, { initialData: currentQuestion, prefilledType: currentConfig?.questionType, prefilledSkill: currentPart?.skill, optionCount: currentConfig?.optionCount, optionMode: currentConfig?.optionMode, defaultValue: currentConfig?.defaultValue, defaultQuestion: currentConfig?.defaultQuestion, viewMode: currentConfig?.viewMode, groups: currentConfig?.groups || normalizedPart?.groups, groupConfig: currentConfig?.groupConfig || normalizedPart?.groupConfig, groupType: currentConfig?.groupType || normalizedPart?.groupType, articleCount: currentConfig?.articleCount ?? currentPart?.articleCount, questionConfig: currentConfig?.questionConfig || currentPart?.questionConfig, questionIndexInPart: isPartLevelGroup ? currentQuestionIndex - (currentPart?.startIndex ?? 0) : currentQuestionIndex - (currentConfig?.startIndex ?? currentPart?.startIndex ?? 0), partId: sharedDataPartId, onChange: (draft) => { draftRef.current = draft; setHasUnsavedChanges(true); }, onSave: handleSaveQuestion, onCancel: onCancel, isEmbedded: true, externalErrors: validationErrors[currentQuestionIndex], validationRef: validationRef, suggestedAnswers: currentConfig?.suggestedAnswers || currentPart?.suggestedAnswers, hasSuggestedAnswers: currentConfig?.hasSuggestedAnswers ?? currentPart?.hasSuggestedAnswers }, currentQuestionIndex)] }) }), _jsx("div", { className: "flex-shrink-0 border-t border-gray-200/80 bg-white/95 shadow-[0_-4px_20px_rgba(0,0,0,0.08)] backdrop-blur-sm", children: _jsxs("div", { className: "mx-auto flex max-w-4xl items-center justify-between px-6 py-4", children: [_jsxs(Button, { variant: "outline", onClick: () => navigateToQuestion(currentQuestionIndex - 1), disabled: currentQuestionIndex === 0, children: [_jsx(ChevronLeft, { className: "mr-2 h-4 w-4" }), "C\u00E2u tr\u01B0\u1EDBc"] }), _jsxs(Button, { onClick: () => void handleSaveQuestion(), disabled: isSavingQuestion, className: "bg-green-600 hover:bg-green-700", children: [_jsx(Save, { className: "mr-2 h-4 w-4" }), isSavingQuestion ? 'Đang lưu...' : 'Lưu câu hỏi'] }), _jsxs(Button, { variant: "outline", onClick: () => navigateToQuestion(currentQuestionIndex + 1), disabled: currentQuestionIndex >= template.totalQuestions - 1, children: ["C\u00E2u ti\u1EBFp", _jsx(ChevronRight, { className: "ml-2 h-4 w-4" })] })] }) })] }), _jsxs("div", { className: "w-80 overflow-y-auto border-l border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm", children: [_jsx(ExamQuestionGrid, { template: template, currentQuestionIndex: currentQuestionIndex, completedQuestions: completedQuestions, onQuestionChange: navigateToQuestion }), _jsxs(Button, { onClick: () => void handleSaveExam(), className: "mt-4 w-full bg-blue-600 hover:bg-blue-700", children: [_jsx(Save, { className: "mr-2 h-4 w-4" }), "Ho\u00E0n t\u1EA5t \u0111\u1EC1 thi"] })] })] }) }));
89
- }
1
+ "use client";import{jsx as e,jsxs as t,Fragment as n}from"react/jsx-runtime";import{useCallback as o,useEffect as i,useMemo as r,useRef as a,useState as s}from"react";import{CheckCircle as d,ChevronLeft as l,ChevronRight as u,Loader2 as c,Save as m}from"lucide-react";import{Button as p}from"../../components/ui/button";import{QuestionBankOption as g}from"../questions/components/QuestionBankOption";import{QuestionSearchDropdown as f}from"../questions/components/QuestionSearchDropdown";import{QuestionCreator as h}from"../questions/creator/QuestionCreator";import{useToast as x}from"../../shared/lib/hooks/useToast";import{useT as b}from"../../shared/lib/i18n";import{useExamQuestionStore as v}from"../../shared/lib/stores/examQuestionStore";import{transformApiQuestionToFrontend as C}from"../../shared/lib/utils/question-reverse-transform";import{ExamPartTabs as w}from"./ExamPartTabs";import{ExamQuestionGrid as y}from"./ExamQuestionGrid";import{PartAudioUploader as N}from"./PartAudioUploader";import{CompletionDialog as I}from"./CompletionDialog";import{UnsavedChangesDialog as k}from"./UnsavedChangesDialog";import{clearQuestionStatuses as q,getQuestionStatus as Q,QuestionStatus as S,setQuestionStatus as E}from"./exam-question-status";import{getEffectiveQuestionConfig as D,getPartByQuestionIndex as T,normalizeTemplateGroupConfig as A}from"./exam-template.utils";export function ExamCreator(L){const{template:B,examId:V=`exam-${Date.now()}`,initialQuestions:j=[]}=L,U=b(),{success:P,warning:$,error:M}=x(),R=a(null),z=a({}),F=a(!1),G=a(!1),H=a(!1),K=v(e=>e.initialized),W=v(e=>e.examId),J=v(e=>e.initializeExam),X=v(e=>e.resetStore),Y=v(e=>e.currentQuestionIndex),Z=v(e=>e.setCurrentQuestionIndex),ee=v(e=>e.questions),te=v(e=>e.upsertQuestionData),ne=v(e=>e.clearQuestionData),oe=v(e=>e.markQuestionCompleted),ie=v(e=>e.getQuestionData),re=v(e=>e.setSharedHintLetters),[ae,se]=s({}),[de,le]=s(!1),[ue,ce]=s(!1),[me,pe]=s(!1),[ge,fe]=s(null),[he,xe]=s(!1),[be,ve]=s(!1),[Ce,we]=s({}),[ye,Ne]=s({}),[Ie,ke]=s({}),[qe,Qe]=s(),[Se,Ee]=s(),De=T(B,Y),Te=De?.id??"",Ae=De?D(De,Y):void 0,Oe=De?A(De):void 0,_e=Boolean(Oe?.groups&&!Ae?.groups),Le=_(Ae),Be=Le&&!_e?`${Te}_${Le}`:Te,Ve=ee[Y]?.data,je=Ce[Y]??{checked:!1,code:"",title:""},Ue=r(()=>{const e=new Set;return Object.entries(ee).forEach(([t,n])=>{n.isCompleted&&e.add(Number(t))}),e},[ee]),Pe=r(()=>({id:V,templateId:B.id,questions:Object.values(ee).map(e=>e.data),currentQuestionIndex:Y,lastSaved:(new Date).toISOString(),createdAt:(new Date).toISOString(),updatedAt:(new Date).toISOString(),isCompleted:Ue.size>=B.totalQuestions}),[Ue.size,Y,V,ee,B.id,B.totalQuestions]);i(()=>{const e=W&&W!==V;e&&X(),K&&!e||J({examId:V,templateId:B.id,totalQuestions:B.totalQuestions})},[V,J,K,X,W,B.id,B.totalQuestions]),i(()=>{if(!K||0===j.length)return;F.current=!0,j.forEach(e=>{const t=(e.questionNumber??1)-1,n={...e,isCompleted:!0};te(t,n),oe(t,!0),z.current[t]=n,function(e,t,n,o){if("LOOK_PICTURE_FILL_WORD_HINT"!==t.type||!t.partId)return;const i=t.answer,r=t.questionGroup?.groupNumber;if(!i?.hintLetters?.length||!r)return;const a=T(n,e),s=a?D(a,e):void 0,d=_(s),l=d?`${t.partId}_${d}`:t.partId;o(l,r,i.hintLetters)}(t,n,B,re);const o=e.questionBank;o?.id&&Ne(e=>({...e,[t]:o}))});const e=window.setTimeout(()=>{F.current=!1},1e3);return()=>window.clearTimeout(e)},[j,K,oe,re,B,te]),i(()=>{pe(Q(V,Y+1)===S.UNSAVED)},[Y,V]),i(()=>{ce(Object.values(ee).some(e=>Object.keys(e.data).length>0&&!e.isCompleted))},[ee]),i(()=>{const e=e=>{ue&&(e.preventDefault(),e.returnValue="")};return window.addEventListener("beforeunload",e),()=>window.removeEventListener("beforeunload",e)},[ue]);const $e=o(e=>{H.current=!0,pe(!1),Z(Math.max(0,Math.min(B.totalQuestions-1,e))),window.setTimeout(()=>{H.current=!1},500)},[Z,B.totalQuestions]),Me=e=>{if(me)return fe(e),void xe(!0);$e(e)},Re=o(e=>{F.current||G.current||H.current||(te(Y,{questionNumber:Y+1,partId:Te,...e}),E(V,Y+1,S.UNSAVED),pe(!0))},[Te,Y,V,te]),ze=async e=>{if(!De)return;if(!function(e,t,n,o){if(!e.checked||t?.id)return!0;const i=e.code.trim()?void 0:"Vui lòng nhập mã câu hỏi cho ngân hàng",r=e.title.trim()?void 0:"Vui lòng nhập tiêu đề câu hỏi cho ngân hàng";return n(i),o(r),!i&&!r}(je,ye[Y],Qe,Ee))return;if(!1===await(R.current?.triggerValidation?.()))return;const t=R.current?.getFormData?.(),n=function(e,t,n,o,i,r,a){return{...n,...e,...t?{answer:t}:{},type:e?.type??n?.type??o,id:n?.id??e?.id,partId:i.id,questionNumber:r+1,difficulty:e?.difficulty??a??n?.difficulty,skill:i.skill,isCompleted:!0}}(e,t,ie(Y),Ae?.questionType,De,Y,L.examDifficulty);le(!0);try{const e=await(L.onSaveQuestion?.({question:n,questionIndex:Y,template:B,examId:V,examDifficulty:L.examDifficulty,examLevel:L.examLevel,subjectId:L.subjectId,sectionId:(o=Ae,o&&"sectionId"in o?o.sectionId:void 0),addToQuestionBank:!ye[Y]?.id&&je.checked,questionBankCode:je.checked?je.code:void 0,questionBankTitle:je.checked?je.title:void 0,existingQuestionBankId:ye[Y]?.id})),t=function(e,t){return e&&"question"in e?{...t,...e.question,isCompleted:!0}:{...t,...e??{},isCompleted:!0}}(e,n);te(Y,t),oe(Y,!0),z.current[Y]=t,function(e,t,n){const o=e&&"questionBank"in e?e.questionBank:e?.questionBank;o?.id&&n(e=>({...e,[t]:o}))}(e,Y,Ne),G.current=!0,window.setTimeout(()=>{G.current=!1},500),E(V,Y+1,S.SAVED),pe(!1),P({message:U("common.examCreator.saveSuccess",{index:String(Y+1)})})}catch(e){M({message:U("common.examCreator.saveError",{index:String(Y+1)}),description:e instanceof Error?e.message:void 0})}finally{le(!1)}var o};return L.saveExamRef&&(L.saveExamRef.current=()=>{me?$({message:U("common.examCreator.saveBeforeExit")}):ve(!0)}),t("div",{className:"flex min-h-0 flex-1 flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50/30",children:[t("div",{className:"flex min-h-0 flex-1 overflow-hidden",children:[e("div",{className:"w-80 overflow-y-auto border-r border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(w,{template:B,currentPartId:Te,completedQuestions:Ue,onPartChange:e=>Me(B.parts.find(t=>t.id===e)?.startIndex??0)})}),t("div",{className:"flex flex-1 flex-col overflow-hidden",children:[e("div",{className:"min-h-0 flex-1 overflow-y-auto p-4 pb-4",children:t("div",{className:"mx-auto max-w-4xl",children:["LISTENING"===De?.skill&&e(N,{partId:De.id,partName:De.name,audioUrl:L.partAudioOverrides?.[De.id]??De.audioUrl??"",examId:V,onAudioChange:e=>{L.onPartAudioChange?.(De.id,e)},isReadOnly:!L.onPartAudioChange}),e(h,{initialData:Ve,prefilledType:Ae?.questionType,prefilledSkill:De?.skill,optionCount:Ae?.optionCount,optionMode:Ae?.optionMode,defaultValue:Ae?.defaultValue,defaultQuestion:Ae?.defaultQuestion,viewMode:Ae?.viewMode,groups:Ae?.groups||Oe?.groups,groupConfig:Ae?.groupConfig||Oe?.groupConfig,groupType:Ae?.groupType||Oe?.groupType,articleCount:Ae?.articleCount??De?.articleCount,questionConfig:Ae?.questionConfig||De?.questionConfig,questionIndexInPart:_e?Y-(De?.startIndex??0):Y-(Ae?.startIndex??De?.startIndex??0),partId:Be,onChange:Re,onUnsavedChangesChange:pe,onSave:ze,onCancel:L.onCancel,isEmbedded:!0,externalErrors:ae[Y],validationRef:R,suggestedAnswers:Ae?.suggestedAnswers||De?.suggestedAnswers,hasSuggestedAnswers:Ae?.hasSuggestedAnswers??De?.hasSuggestedAnswers,headerExtra:e(f,{questionType:Ae?.questionType||De?.questionType,onSelect:e=>{const t={...C({question_type:e.type,content:e.content,correct_answer:e.answer,points:e.points,difficult_level:e.difficulty,question_skill_type:e.skill,explanation:e.explanation}),id:void 0,partId:Te,questionNumber:Y+1,isCompleted:!1};te(Y,t);const n=e.id,o=e.code,i=e.title;(o||i)&&ke(e=>({...e,[Y]:{code:o,title:i}})),n&&(o||i)&&Ne(e=>({...e,[Y]:{id:n,code:o??"",title:i??""}})),E(V,Y+1,S.UNSAVED),pe(!0),oe(Y,!1),P({message:U("common.examCreator.bankSelected")})},selectedQuestionLabel:Ie[Y]?`${Ie[Y].code?`#${Ie[Y].code} - `:""}${Ie[Y].title||""}`:void 0,disabled:!(Ae?.questionType||De?.questionType),compact:!0,searchQuestions:L.searchQuestions})},Y),e("div",{className:"mt-8",children:e(g,{checked:je.checked,onCheckedChange:e=>O(Y,{checked:e},we,pe,V),code:je.code,onCodeChange:e=>O(Y,{code:e},we,pe,V),title:je.title,onTitleChange:e=>O(Y,{title:e},we,pe,V),codeError:qe,error:Se,existingBank:ye[Y]||null})})]})}),e("div",{className:"flex-shrink-0 border-t border-gray-200/80 bg-white/95 shadow-[0_-4px_20px_rgba(0,0,0,0.08)] backdrop-blur-sm",children:t("div",{className:"mx-auto flex max-w-4xl items-center justify-between px-6 py-4",children:[t(p,{variant:"outline",onClick:()=>Me(Y-1),disabled:0===Y,className:"gap-2 border-gray-300 hover:bg-gray-50",children:[e(l,{className:"h-4 w-4"}),"Câu trước"]}),t("div",{className:"flex items-center gap-4",children:[me?t("div",{className:"flex items-center gap-2 rounded-full bg-amber-50 px-3 py-1.5 ring-1 ring-amber-200",children:[e("span",{className:"h-2 w-2 animate-pulse rounded-full bg-amber-500"}),e("span",{className:"text-xs font-medium text-amber-700",children:"Chưa lưu"})]}):ee[Y]?.isCompleted?t("div",{className:"flex items-center gap-2 rounded-full bg-green-50 px-3 py-1.5 ring-1 ring-green-200",children:[e(d,{className:"h-3.5 w-3.5 text-green-500"}),e("span",{className:"text-xs font-medium text-green-700",children:"Đã lưu"})]}):null,t("div",{className:"flex items-center gap-2 rounded-full bg-gray-100 px-4 py-2",children:[t("span",{className:"text-sm font-bold text-gray-900",children:["Câu ",Y+1]}),e("span",{className:"text-sm text-gray-400",children:"/"}),e("span",{className:"text-sm text-gray-500",children:B.totalQuestions})]}),e(p,{onClick:()=>{ze(ie(Y))},disabled:de||!me,className:"gap-2 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 shadow-md hover:shadow-lg transition-all",children:t(n,de?{children:[e(c,{className:"h-4 w-4 animate-spin"}),"Đang lưu..."]}:{children:[e(m,{className:"h-4 w-4"}),"Lưu câu hỏi"]})})]}),t(p,{variant:"outline",onClick:()=>Me(Y+1),disabled:Y>=B.totalQuestions-1||!ee[Y]?.isCompleted,className:"gap-2 border-gray-300 hover:bg-gray-50",children:["Câu tiếp theo",e(u,{className:"h-4 w-4"})]})]})})]}),e("div",{className:"w-80 overflow-y-auto border-l border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(y,{template:B,currentQuestionIndex:Y,completedQuestions:Ue,onQuestionChange:Me})})]}),e(I,{open:be,onOpenChange:ve,completedCount:Ue.size,totalQuestions:B.totalQuestions,onConfirm:()=>{ve(!1),q(V),X(),L.onSave?.(Pe)}}),e(k,{open:he,onOpenChange:xe,onConfirm:()=>{const e=z.current[Y];e?(te(Y,e),E(V,Y+1,S.SAVED)):ne(Y),pe(!1),null!==ge&&$e(ge),fe(null),xe(!1)},onCancel:()=>xe(!1),questionNumber:Y+1})]})}function O(e,t,n,o,i){n(n=>{const o=n[e]??{checked:!1,code:"",title:""};return{...n,[e]:{...o,...t}}}),o(!0),E(i,e+1,S.UNSAVED)}function _(e){return e&&"sectionName"in e?e.sectionName:void 0}
@@ -5,5 +5,5 @@ interface ExamPartTabsProps {
5
5
  completedQuestions: Set<number>;
6
6
  onPartChange: (partId: string) => void;
7
7
  }
8
- export declare function ExamPartTabs({ template, currentPartId, completedQuestions, onPartChange }: ExamPartTabsProps): import("react").JSX.Element;
8
+ export declare function ExamPartTabs({ template, currentPartId, completedQuestions, onPartChange, }: ExamPartTabsProps): import("react").JSX.Element;
9
9
  export {};
@@ -1,40 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { CheckCircle2, Circle, Layers, Lock, Play } from 'lucide-react';
4
- import { cn } from '../../shared/lib/utils';
5
- export function ExamPartTabs({ template, currentPartId, completedQuestions, onPartChange }) {
6
- const getProgress = (part) => {
7
- const total = part.endIndex - part.startIndex + 1;
8
- let completed = 0;
9
- for (let i = part.startIndex; i <= part.endIndex; i++)
10
- if (completedQuestions.has(i))
11
- completed++;
12
- return { completed, total, percentage: total > 0 ? Math.round((completed / total) * 100) : 0 };
13
- };
14
- const getStatus = (part) => {
15
- const progress = getProgress(part);
16
- if (progress.completed === 0)
17
- return 'not-started';
18
- if (progress.completed === progress.total)
19
- return 'completed';
20
- return 'in-progress';
21
- };
22
- const isUnlocked = (partIndex) => template.parts.slice(0, partIndex).every((part) => getStatus(part) === 'completed');
23
- return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2 px-1", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600 shadow-sm", children: _jsx(Layers, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-bold text-gray-900", children: "C\u00E1c ph\u1EA7n thi" }), _jsxs("p", { className: "text-xs text-gray-500", children: [template.parts.length, " ph\u1EA7n"] })] })] }), _jsx("div", { className: "space-y-3", children: template.parts.map((part, index) => {
24
- const progress = getProgress(part);
25
- const status = getStatus(part);
26
- const active = part.id === currentPartId;
27
- const unlocked = isUnlocked(index);
28
- return (_jsx("button", { onClick: () => unlocked && onPartChange(part.id), disabled: !unlocked, className: cn('w-full rounded-xl border-2 p-4 text-left transition-all', active && unlocked ? 'border-transparent bg-blue-600 text-white shadow-lg' : 'border-gray-200 bg-white hover:border-gray-300', !unlocked && 'cursor-not-allowed opacity-60'), children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx(StatusIcon, { status: status, active: active, unlocked: unlocked }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "truncate text-sm font-semibold", children: part.name }), _jsxs("span", { className: "rounded-full bg-black/10 px-2 py-0.5 text-xs font-bold", children: [progress.completed, "/", progress.total] })] }), part.description && _jsx("p", { className: cn('mt-1 text-xs', active ? 'text-blue-100' : 'text-gray-500'), children: part.description }), _jsx("div", { className: cn('mt-2 h-1.5 overflow-hidden rounded-full', active ? 'bg-white/20' : 'bg-gray-200'), children: _jsx("div", { className: cn('h-full rounded-full', active ? 'bg-white' : 'bg-green-500'), style: { width: `${progress.percentage}%` } }) })] })] }) }, part.id));
29
- }) })] }));
30
- }
31
- function StatusIcon({ status, active, unlocked }) {
32
- const className = cn('h-4 w-4', active ? 'text-white' : 'text-gray-500');
33
- if (!unlocked)
34
- return _jsx(Lock, { className: className });
35
- if (status === 'completed')
36
- return _jsx(CheckCircle2, { className: className });
37
- if (status === 'in-progress')
38
- return _jsx(Play, { className: className });
39
- return _jsx(Circle, { className: className });
40
- }
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{CheckCircle2 as r,ChevronRight as a,Circle as s,Layers as n,Lock as l,Play as i}from"lucide-react";import{cn as o}from"../../shared/lib/utils";export function ExamPartTabs({template:d,currentPartId:c,completedQuestions:m,onPartChange:g}){const h=e=>{let t=0;for(let r=e.startIndex;r<=e.endIndex;r++)m.has(r)&&t++;const r=e.endIndex-e.startIndex+1;return{completed:t,total:r,percentage:r>0?Math.round(t/r*100):0}},b=e=>{const t=h(e);return 0===t.completed?"not-started":t.completed===t.total?"completed":"in-progress"},x=e=>{if(0===e)return!0;for(let t=0;t<e;t++){const e=d.parts[t];if("completed"!==b(e))return!1}return!0},p=(t,a,n)=>n?"completed"===t?e(r,{className:o("h-4 w-4",a?"text-white":"text-green-500")}):"in-progress"===t?e(i,{className:o("h-4 w-4",a?"text-white":"text-amber-500")}):e(s,{className:o("h-4 w-4",a?"text-white/70":"text-gray-400")}):e(l,{className:"h-4 w-4 text-gray-400"}),u=(r,s,n,i,d,c)=>t("button",{onClick:()=>c&&g(r.id),disabled:!c,className:o("group relative w-full rounded-xl border-2 p-4 text-left transition-all duration-200","focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none",{"border-transparent bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow-lg shadow-blue-200/50":d&&c,"border-green-200 bg-gradient-to-r from-green-50 to-emerald-50 hover:border-green-300 hover:shadow-md":!d&&"completed"===i&&c,"border-amber-200 bg-gradient-to-r from-amber-50 to-orange-50 hover:border-amber-300 hover:shadow-md":!d&&"in-progress"===i&&c,"border-gray-200 bg-white hover:border-gray-300 hover:shadow-md":!d&&"not-started"===i&&c,"cursor-not-allowed border-gray-100 bg-gray-50/50 opacity-60":!c}),children:[d&&e("div",{className:"absolute -left-[2px] top-1/2 h-8 w-1 -translate-y-1/2 rounded-full bg-white shadow-sm"}),t("div",{className:"flex items-start gap-3",children:[e("div",{className:o("flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg transition-colors",d?"bg-white/20":"completed"===i?"bg-green-100":"in-progress"===i?"bg-amber-100":"bg-gray-100"),children:p(i,d,c)}),t("div",{className:"flex-1 min-w-0",children:[t("div",{className:"flex items-center justify-between gap-2",children:[e("span",{className:o("text-sm font-semibold truncate",d?"text-white":"completed"===i?"text-green-800":"in-progress"===i?"text-amber-800":"text-gray-800"),children:r.name}),t("span",{className:o("flex-shrink-0 rounded-full px-2 py-0.5 text-xs font-bold",d?"bg-white/20 text-white":"completed"===i?"bg-green-100 text-green-700":"in-progress"===i?"bg-amber-100 text-amber-700":"bg-gray-100 text-gray-600"),children:[n.completed,"/",n.total]})]}),r.description&&e("p",{className:o("mt-1 text-xs leading-relaxed",d?"text-blue-100":"text-gray-500"),children:r.description}),t("div",{className:"mt-2",children:[e("div",{className:o("h-1.5 overflow-hidden rounded-full",d?"bg-white/20":"bg-gray-200"),children:e("div",{className:o("h-full rounded-full transition-all duration-500 ease-out",d?"bg-white":"completed"===i?"bg-gradient-to-r from-green-400 to-emerald-500":"in-progress"===i?"bg-gradient-to-r from-amber-400 to-orange-500":"bg-gray-300"),style:{width:`${n.percentage}%`}})}),t("div",{className:o("mt-1 flex items-center justify-between text-xs",d?"text-blue-100":"text-gray-500"),children:[t("span",{children:[r.questionCount," câu hỏi"]}),t("span",{className:"font-medium",children:[n.percentage,"%"]})]})]}),r.isGroup&&r.groups&&r.groups.length>0&&t("div",{className:o("mt-2 rounded-lg p-2 text-xs",d?"bg-white/10":"bg-gray-100/80"),children:[t("div",{className:o("mb-1 font-medium",d?"text-blue-100":"text-gray-600"),children:[r.groups.length," nhóm câu hỏi:"]}),e("div",{className:"space-y-0.5",children:r.groups.map((r,s)=>t("div",{className:o("flex items-center justify-between",d?"text-blue-100":"text-gray-500"),children:[t("span",{className:"flex items-center gap-1",children:[e(a,{className:"h-3 w-3"}),"Nhóm ",r.number]}),t("span",{className:"font-medium",children:[r.questionCount," câu"]})]},s))})]}),r.sections&&r.sections.length>0&&t("div",{className:o("mt-2 rounded-lg p-2 text-xs",d?"bg-white/10":"bg-gray-100/80"),children:[t("div",{className:o("mb-1 font-medium",d?"text-blue-100":"text-gray-600"),children:[r.sections.length," phần nhỏ:"]}),e("div",{className:"space-y-1",children:r.sections.map((r,s)=>{let n=0;for(let e=r.startIndex;e<=r.endIndex;e++)m.has(e)&&n++;const l=r.endIndex-r.startIndex+1;return t("div",{className:o("flex items-center justify-between",d?"text-blue-100":"text-gray-500"),children:[t("span",{className:"flex items-center gap-1 truncate flex-1 mr-2",children:[e(a,{className:"h-3 w-3 flex-shrink-0"}),e("span",{className:"truncate",children:r.name})]}),t("span",{className:"font-medium flex-shrink-0",children:[n,"/",l]})]},s)})})]}),!c&&t("div",{className:"mt-2 flex items-center gap-1 text-xs text-gray-400",children:[e(l,{className:"h-3 w-3"}),e("span",{children:"Hoàn thành phần trước để mở khóa"})]})]})]})]},r.id);return t("div",{className:"space-y-4",children:[t("div",{className:"flex items-center gap-2 px-1",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600 shadow-sm",children:e(n,{className:"h-4 w-4 text-white"})}),t("div",{children:[e("h3",{className:"text-sm font-bold text-gray-900",children:"Các phần thi"}),t("p",{className:"text-xs text-gray-500",children:[d.parts.length," phần"]})]})]}),e("div",{className:"space-y-4",children:(()=>{const r=[];d.parts.forEach((e,t)=>{const a=e.category||"",s=r[r.length-1];s&&s.groupKey===a?s.parts.push({part:e,originalIndex:t}):r.push({groupKey:a,parts:[{part:e,originalIndex:t}]})});const a={LISTENING:"🎧 LISTENING",READING_AND_WRITING:"📝 READING AND WRITING",READING:"📖 READING",WRITING:"✍️ WRITING",SPEAKING:"🗣️ SPEAKING"},s={LISTENING:{bg:"bg-gradient-to-r from-pink-100 via-rose-100 to-pink-50",border:"border-pink-300",text:"text-pink-900",connector:"bg-pink-300"},READING_AND_WRITING:{bg:"bg-gradient-to-r from-teal-100 via-cyan-100 to-teal-50",border:"border-teal-300",text:"text-teal-900",connector:"bg-teal-300"},READING:{bg:"bg-gradient-to-r from-amber-100 via-yellow-100 to-amber-50",border:"border-amber-300",text:"text-amber-900",connector:"bg-amber-300"},WRITING:{bg:"bg-gradient-to-r from-indigo-100 via-blue-100 to-indigo-50",border:"border-indigo-300",text:"text-indigo-900",connector:"bg-indigo-300"},SPEAKING:{bg:"bg-gradient-to-r from-violet-100 via-purple-100 to-violet-50",border:"border-violet-300",text:"text-violet-900",connector:"bg-violet-300"}};return r.length<=1&&!d.parts.some(e=>e.category)?d.parts.map((e,t)=>{const r=h(e),a=b(e),s=c===e.id,n=x(t);return u(e,0,r,a,s,n)}):r.map((r,n)=>{const l=s[r.groupKey]||{bg:"bg-gray-50",border:"border-gray-200",text:"text-gray-800",connector:"bg-gray-300"},i=a[r.groupKey]||r.groupKey;return t("div",{className:"space-y-2",children:[e("div",{className:o("rounded-lg border-2 px-3 py-2 text-center",l.bg,l.border),children:e("span",{className:o("text-xs font-bold tracking-wide uppercase",l.text),children:i})}),e("div",{className:"relative ml-3 pl-5",children:e("div",{className:"space-y-3",children:r.parts.map(({part:a,originalIndex:s},n)=>{const i=h(a),d=b(a),m=c===a.id,g=x(s),p=0===n,N=n===r.parts.length-1;return t("div",{className:"relative",children:[!p&&e("div",{className:o("absolute -left-5 top-0 h-1/2 w-[2px]",l.connector)}),!N&&e("div",{className:o("absolute -left-5 top-1/2 w-[2px]",l.connector),style:{height:"calc(50% + 12px)"}}),p&&e("div",{className:o("absolute -left-5 -top-2 h-[calc(50%+8px)] w-[2px]",l.connector)}),e("div",{className:o("absolute -left-5 top-1/2 -translate-y-1/2 h-[2px] w-5",l.connector)}),u(a,0,i,d,m,g)]},a.id)})})})]},`group-${n}`)})})()}),t("div",{className:"rounded-xl border border-gray-200 bg-gradient-to-br from-gray-50 to-slate-100 p-4 shadow-sm",children:[e("h4",{className:"mb-3 text-xs font-bold uppercase tracking-wide text-gray-500",children:"Tổng quan"}),t("div",{className:"space-y-2",children:[t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2",children:[e("div",{className:"h-2 w-2 rounded-full bg-blue-500"}),e("span",{className:"text-sm text-gray-600",children:"Tổng câu hỏi"})]}),e("span",{className:"text-sm font-bold text-gray-900",children:d.totalQuestions})]}),t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2",children:[e("div",{className:"h-2 w-2 rounded-full bg-green-500"}),e("span",{className:"text-sm text-gray-600",children:"Đã hoàn thành"})]}),e("span",{className:"text-sm font-bold text-green-600",children:m.size})]}),t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2",children:[e("div",{className:"h-2 w-2 rounded-full bg-amber-500"}),e("span",{className:"text-sm text-gray-600",children:"Còn lại"})]}),e("span",{className:"text-sm font-bold text-amber-600",children:d.totalQuestions-m.size})]})]}),t("div",{className:"mt-3 pt-3 border-t border-gray-200",children:[t("div",{className:"flex items-center justify-between mb-1",children:[e("span",{className:"text-xs text-gray-500",children:"Tiến độ tổng"}),t("span",{className:"text-xs font-bold text-blue-600",children:[Math.round(m.size/d.totalQuestions*100),"%"]})]}),e("div",{className:"h-2 overflow-hidden rounded-full bg-gray-200",children:e("div",{className:"h-full rounded-full bg-gradient-to-r from-blue-500 to-indigo-600 transition-all duration-500",style:{width:m.size/d.totalQuestions*100+"%"}})})]})]})]})}
@@ -0,0 +1,20 @@
1
+ import type { ExamQuestionsSdkApi } from '../../api/exam-questions.types';
2
+ export interface ExamPreviewDialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ /** Exam (đề) to preview. `null` while nothing is selected. */
6
+ examId: string | null;
7
+ /** Questions API adapter — provides `getExam` + `getQuestions`. */
8
+ api: ExamQuestionsSdkApi;
9
+ /** Fallback heading shown until the exam name loads. */
10
+ title?: string;
11
+ }
12
+ /**
13
+ * Read-only popup that previews an exam (đề) by id: the layout mirrors the
14
+ * exam taking experience (left sidebar with exam info + progress + question
15
+ * navigation, right pane showing one question at a time) but creates NO attempt
16
+ * — exam + questions are fetched through {@link ExamQuestionsSdkApi}, so it
17
+ * matches exactly how the đề looks. Each question renders via {@link QuestionViewer}
18
+ * in review mode (correct answers highlighted).
19
+ */
20
+ export declare function ExamPreviewDialog({ open, onOpenChange, examId, api, title }: ExamPreviewDialogProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useEffect as a,useMemo as n,useRef as i,useState as s}from"react";import{ArrowLeft as r,ArrowRight as l,Eye as d,Loader2 as m}from"lucide-react";import{Badge as c}from"../../components/ui/badge";import{Button as o}from"../../components/ui/button";import{Card as p,CardContent as x,CardHeader as u,CardTitle as h}from"../../components/ui/card";import{Dialog as g,DialogContent as f,DialogHeader as N,DialogTitle as v}from"../../components/ui/dialog";import{Progress as b}from"../../components/ui/progress";import{useT as y}from"../../shared/lib/i18n";import{QuestionViewer as w}from"../questions/viewer/QuestionViewer";function j(e){return e&&"object"==typeof e&&"data"in e?e.data:e}function I(e){switch(e?.toUpperCase()){case"EASY":return"bg-green-100 text-green-800";case"MEDIUM":return"bg-orange-100 text-orange-800";case"HARD":return"bg-red-100 text-red-800";default:return"bg-gray-100 text-gray-800"}}export function ExamPreviewDialog({open:q,onOpenChange:S,examId:C,api:Q,title:k}){const E=y(),[P,z]=s(null),[A,M]=s([]),[$,D]=s(0),[T,O]=s("all"),[R,U]=s(!1),[H,V]=s(!1),[Y,B]=s(null),[F,G]=s(0),J=i(null);a(()=>{if(!q||!C)return;let e=!1;return V(!0),B(null),D(0),O("all"),(async()=>{try{const t=j(await Q.getExam(C)),a=Q.getQuestions?j(await Q.getQuestions(C)):[];if(e)return;const n=[...a].sort((e,t)=>(e.questionNumber??0)-(t.questionNumber??0));z(t),M(n)}catch{e||B(E("admin.exams.preview.loadError"))}finally{e||V(!1)}})(),()=>{e=!0}},[q,C,F,Q,E]),a(()=>{q||(z(null),M([]),D(0),O("all"),U(!1),B(null))},[q]);const K=n(()=>{const e=(P?.template?.parts??[]).map(e=>({id:e.id,name:e.name||e.title})),t=new Set(e.map(e=>e.id));for(const a of A){const n=a.partId??"";n&&!t.has(n)&&(t.add(n),e.push({id:n}))}return e},[P,A]),L=e=>{const t=K.findIndex(t=>t.id===(e??""));return t>=0?t+1:1},W=n(()=>{const e=new Map;for(const t of A){const a=t.partId??"";if(!e.has(a)){const t=K.findIndex(e=>e.id===a),n=t>=0?t+1:1;e.set(a,{partId:a,partNo:n,partName:K[t]?.name||`Part ${n}`,questions:[]})}e.get(a).questions.push(t)}return Array.from(e.values()).sort((e,t)=>e.partNo-t.partNo)},[A,K]),X=P?.name??k??"",Z=P?.template?.totalTime??P?.duration??0,_=function(e){return e.reduce((e,t)=>e+(Number(t.points)||0),0)}(A),ee=A[$],te=A.length>0?($+1)/A.length*100:0,ae=e=>{D(e),J.current?.scrollTo({top:0,behavior:"smooth"})},ne="all"===T?A:A.filter(e=>L(e.partId)===T);return e(g,{open:q,onOpenChange:S,children:t(f,{className:"max-h-[95vh] max-w-7xl overflow-y-auto",children:[e(N,{children:t(v,{className:"flex items-center gap-2",children:[e(d,{className:"h-5 w-5 text-blue-600"}),E("admin.exams.preview.title",{name:X})]})}),t("div",{className:"flex h-[80vh] flex-col gap-4 sm:flex-row",children:[e("div",{className:"w-full overflow-y-auto border-b border-gray-200 sm:w-80 sm:border-b-0 sm:border-r",children:t("div",{className:"space-y-4 p-4",children:[t(p,{children:[e(u,{className:"pb-3",children:e(h,{className:"text-base",children:E("admin.exams.preview.examInfo")})}),t(x,{className:"space-y-3",children:[t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-sm text-gray-600",children:E("admin.exams.preview.duration")}),e("span",{className:"text-sm font-medium",children:E("admin.exams.view.minutes",{count:String(Z)})})]}),t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-sm text-gray-600",children:E("admin.exams.preview.questionCount")}),e("span",{className:"text-sm font-medium",children:H?"...":E("admin.exams.preview.questions",{count:String(A.length)})})]}),t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-sm text-gray-600",children:E("admin.exams.preview.totalPoints")}),e("span",{className:"text-sm font-medium",children:E("admin.exams.preview.points",{count:String(_)})})]})]})]}),!H&&A.length>0&&e(p,{children:e(x,{className:"p-4",children:t("div",{className:"space-y-2",children:[t("div",{className:"flex justify-between text-sm",children:[e("span",{children:E("admin.exams.preview.progress")}),t("span",{className:"font-medium",children:[$+1,"/",A.length]})]}),e(b,{value:te,className:"h-2"})]})})}),H?e("div",{className:"flex items-center justify-center py-4",children:t("div",{className:"flex items-center gap-2 text-gray-500",children:[e(m,{className:"h-4 w-4 animate-spin"}),e("span",{className:"text-sm",children:E("admin.exams.preview.loading")})]})}):A.length>0&&t("div",{className:"space-y-3",children:[e("h4",{className:"text-sm font-medium text-gray-700",children:E("admin.exams.preview.questionNavigation")}),t("div",{className:"flex flex-wrap gap-2",children:[e(o,{variant:"all"===T?"default":"outline",size:"sm",onClick:()=>O("all"),className:"h-8 px-3 text-xs font-medium",children:E("admin.exams.preview.all")}),W.map(e=>t(o,{variant:T===e.partNo?"default":"outline",size:"sm",onClick:()=>{O(e.partNo);const t=e.questions[0];if(t){const e=A.findIndex(e=>e.id===t.id);-1!==e&&ae(e)}},className:"h-8 px-3 text-xs font-medium",children:["P",e.partNo]},e.partId||`part-${e.partNo}`))]}),e("div",{className:"rounded bg-gray-50 p-2 text-xs",children:e("span",{className:"font-medium",children:"all"===T?E("admin.exams.preview.allQuestions",{count:String(A.length)}):E("admin.exams.preview.partQuestions",{part:String(T),count:String(W.find(e=>e.partNo===T)?.questions.length??0)})})}),e("div",{className:"rounded-lg bg-gray-50 p-3",children:e("div",{className:"grid max-h-64 grid-cols-6 gap-2 overflow-y-auto pr-2",children:ne.map(t=>{const a=A.findIndex(e=>e.id===t.id);return e(o,{variant:"ghost",size:"sm",onClick:()=>ae(a),className:"h-8 w-full p-0 text-xs font-medium transition-all duration-200 "+(a===$?"border-2 border-blue-500 bg-blue-500 text-white hover:bg-blue-600":"border border-gray-300 bg-white hover:bg-gray-100"),children:t.questionNumber},t.id)})})})]}),!H&&A.length>0&&t("div",{children:[t("div",{className:"mb-2 flex items-center justify-between",children:[e("h4",{className:"text-sm font-medium text-gray-700",children:X}),e(o,{variant:"ghost",size:"sm",onClick:()=>U(e=>!e),className:"h-6 px-2 text-xs text-gray-600 hover:text-gray-900",children:E(R?"admin.exams.preview.collapse":"admin.exams.preview.detail")})]}),e("div",{className:"rounded-lg bg-gray-50 p-3",children:t("div",{className:"flex items-center justify-between",children:[t("div",{children:[t("span",{className:"text-sm font-medium text-gray-900",children:["Total: ",E("admin.exams.preview.questions",{count:String(A.length)})]}),e("div",{className:"mt-1 flex gap-3",children:t("span",{className:"text-xs text-gray-600",children:[W.length," part(s)"]})})]}),e("div",{className:"text-xs text-gray-500",children:E("admin.exams.view.minutes",{count:String(Z)})})]})}),R&&e("div",{className:"mt-3 space-y-2",children:W.map(a=>{const n=L(ee?.partId)===a.partNo;return e("div",{className:"cursor-pointer rounded-lg border-2 p-3 transition-all duration-200 "+(n?"border-blue-400 bg-blue-50 shadow-md":"border-gray-200 bg-white hover:border-gray-300"),onClick:()=>{const e=a.questions[0];if(e){const t=A.findIndex(t=>t.id===e.id);-1!==t&&(ae(t),O(a.partNo))}},children:t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-sm font-medium "+(n?"text-blue-900":"text-gray-900"),children:a.partName}),e(c,{variant:"secondary",className:"text-xs "+(n?"bg-blue-200 text-blue-800":"bg-gray-100 text-gray-700"),children:E("admin.exams.preview.questions",{count:String(a.questions.length)})})]})},a.partId||`part-${a.partNo}`)})})]})]})}),e("div",{className:"flex-1 overflow-y-auto",children:t("div",{className:"flex h-full flex-col",children:[t("div",{className:"flex items-center justify-between border-b pb-4",children:[t("div",{children:[e("h3",{className:"text-lg font-semibold",children:H?e("span",{className:"text-gray-400",children:E("admin.exams.preview.loading")}):A.length>0?E("admin.exams.preview.questionIndex",{index:String($+1),total:String(A.length)}):E("admin.exams.preview.noQuestions")}),ee&&e("div",{className:"mt-1 inline-flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5",children:e("span",{className:"text-sm font-semibold text-blue-700",children:(e=>{const t=K.findIndex(t=>t.id===(e??""));return K[t]?.name||`Part ${t>=0?t+1:1}`})(ee.partId)})})]}),ee&&t("div",{className:"flex items-center gap-2",children:[e(c,{variant:"outline",children:E("admin.exams.preview.points",{count:String(Number(ee.points)||1)})}),e(c,{className:I(ee.difficulty),children:ee.difficulty??"N/A"})]})]}),e("div",{ref:J,className:"flex-1 overflow-y-auto py-4",children:H?t("div",{className:"flex flex-col items-center justify-center space-y-4 py-12",children:[e(m,{className:"h-8 w-8 animate-spin text-blue-500"}),e("p",{className:"text-gray-600",children:E("admin.exams.preview.loadingQuestions")})]}):Y?e("div",{className:"flex flex-col items-center justify-center space-y-4 py-12",children:t("div",{className:"rounded-lg border border-red-200 bg-red-50 p-6 text-center",children:[e("p",{className:"font-medium text-red-800",children:Y}),e(o,{variant:"outline",onClick:()=>G(e=>e+1),className:"mt-4",children:E("admin.exams.preview.retry")})]})}):0===A.length?e("div",{className:"flex flex-col items-center justify-center space-y-4 py-12",children:e("div",{className:"rounded-lg border border-yellow-200 bg-yellow-50 p-6 text-center",children:e("p",{className:"font-medium text-yellow-800",children:E("admin.exams.preview.examNoQuestions")})})}):ee?e(w,{question:ee,isReviewMode:!0}):null}),!H&&A.length>0&&t("div",{className:"mt-auto flex items-center justify-between border-t border-gray-200 bg-gray-50 px-4 py-3",children:[t(o,{variant:"outline",onClick:()=>{$>0&&ae($-1)},disabled:0===$,className:"flex h-9 items-center gap-2 px-3 text-sm",children:[e(r,{className:"h-4 w-4"}),e("span",{className:"hidden sm:inline",children:E("admin.exams.preview.prevQuestion")}),e("span",{className:"sm:hidden",children:E("admin.exams.preview.prev")})]}),t("div",{className:"text-sm text-gray-600",children:[$+1," / ",A.length]}),t(o,{variant:"outline",onClick:()=>{$<A.length-1&&ae($+1)},disabled:$===A.length-1,className:"flex h-9 items-center gap-2 px-3 text-sm",children:[e("span",{className:"hidden sm:inline",children:E("admin.exams.preview.nextQuestion")}),e("span",{className:"sm:hidden",children:E("admin.exams.preview.next")}),e(l,{className:"h-4 w-4"})]})]})]})})]}),e("div",{className:"border-t pt-4",children:t("div",{className:"flex items-center justify-between",children:[e("div",{className:"text-sm text-gray-600",children:E("admin.exams.preview.footerNote")}),e(o,{variant:"outline",onClick:()=>S(!1),className:"border-gray-300",children:E("admin.exams.preview.close")})]})})]})})}
@@ -5,5 +5,5 @@ interface ExamQuestionGridProps {
5
5
  completedQuestions: Set<number>;
6
6
  onQuestionChange: (questionIndex: number) => void;
7
7
  }
8
- export declare function ExamQuestionGrid({ template, currentQuestionIndex, completedQuestions, onQuestionChange }: ExamQuestionGridProps): import("react").JSX.Element;
8
+ export declare function ExamQuestionGrid({ template, currentQuestionIndex, completedQuestions, onQuestionChange, }: ExamQuestionGridProps): import("react").JSX.Element;
9
9
  export {};
@@ -1,18 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { CheckCircle2, Circle, ListOrdered, Zap } from 'lucide-react';
4
- import { cn } from '../../shared/lib/utils';
5
- import { getPartByQuestionIndex, getSectionForQuestionIndex } from './exam-template.utils';
6
- export function ExamQuestionGrid({ template, currentQuestionIndex, completedQuestions, onQuestionChange }) {
7
- return (_jsxs("div", { className: "space-y-5", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-9 w-9 items-center justify-center rounded-lg bg-blue-600 shadow-sm", children: _jsx(ListOrdered, { className: "h-4 w-4 text-white" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-bold text-gray-900", children: "Danh s\u00E1ch c\u00E2u h\u1ECFi" }), _jsx("p", { className: "text-xs text-gray-500", children: "Click \u0111\u1EC3 chuy\u1EC3n c\u00E2u" })] })] }), _jsx("div", { className: "rounded-xl border border-gray-200 bg-gradient-to-b from-gray-50 to-white p-4 shadow-inner", children: _jsx("div", { className: "grid max-h-[320px] grid-cols-6 justify-center gap-3 overflow-y-auto px-3 py-2", children: Array.from({ length: template.totalQuestions }, (_, index) => {
8
- const part = getPartByQuestionIndex(template, index);
9
- const section = part ? getSectionForQuestionIndex(part, index) : undefined;
10
- return _jsx("button", { onClick: () => onQuestionChange(index), className: buttonClass(index, currentQuestionIndex, completedQuestions), title: [`Câu ${index + 1}`, part?.name, section?.name].filter(Boolean).join(' - '), children: index + 1 }, index);
11
- }) }) }), _jsxs("div", { className: "rounded-xl border border-gray-100 bg-white p-3 shadow-sm", children: [_jsx("div", { className: "mb-2 text-xs font-bold uppercase tracking-wide text-gray-400", children: "Ch\u00FA th\u00EDch" }), _jsx(Legend, { icon: _jsx(Zap, { className: "h-3.5 w-3.5 text-white" }), label: "C\u00E2u \u0111ang l\u00E0m", className: "bg-blue-600" }), _jsx(Legend, { icon: _jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-white" }), label: "\u0110\u00E3 ho\u00E0n th\u00E0nh", className: "bg-green-500" }), _jsx(Legend, { icon: _jsx(Circle, { className: "h-3.5 w-3.5 text-gray-400" }), label: "Ch\u01B0a t\u1EA1o", className: "border bg-white" })] })] }));
12
- }
13
- function buttonClass(index, current, completed) {
14
- return cn('h-8 w-8 rounded-lg border-2 text-xs font-semibold shadow-sm transition-all hover:scale-105', index === current && 'border-transparent bg-blue-600 text-white ring-2 ring-blue-400', index !== current && completed.has(index) && 'border-transparent bg-green-500 text-white', index !== current && !completed.has(index) && 'border-gray-200 bg-white text-gray-500 hover:bg-gray-50');
15
- }
16
- function Legend({ icon, label, className }) {
17
- return _jsxs("div", { className: "mb-2 flex items-center gap-3", children: [_jsx("div", { className: cn('flex h-7 w-7 items-center justify-center rounded-lg', className), children: icon }), _jsx("span", { className: "text-xs font-medium text-gray-700", children: label })] });
18
- }
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{CheckCircle2 as r,Circle as a,ListOrdered as s,Zap as n}from"lucide-react";import{cn as i}from"../../shared/lib/utils";import{getPartByQuestionIndex as o,getSectionForQuestionIndex as d}from"./exam-template.utils";export function ExamQuestionGrid({template:l,currentQuestionIndex:c,completedQuestions:m,onQuestionChange:h}){const g=e=>{const t=(e=>e===c?"current":m.has(e)?"completed":"not-started")(e);return i("h-8 w-8 rounded-lg text-xs font-semibold transition-all duration-200","hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-1","border-2 cursor-pointer shadow-sm",{"bg-gradient-to-br from-blue-500 to-indigo-600 text-white border-transparent shadow-lg shadow-blue-300/50 ring-2 ring-blue-400 ring-offset-1":"current"===t,"bg-gradient-to-br from-green-400 to-emerald-500 text-white border-transparent hover:shadow-md hover:shadow-green-200":"completed"===t,"bg-white text-gray-500 border-gray-200 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700":"not-started"===t})},u=Array.from({length:l.totalQuestions},(e,t)=>t);return t("div",{className:"space-y-5",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-9 w-9 items-center justify-center rounded-lg bg-blue-600 shadow-sm",children:e(s,{className:"h-4 w-4 text-white"})}),t("div",{children:[e("h3",{className:"text-sm font-bold text-gray-900",children:"Danh sách câu hỏi"}),e("p",{className:"text-xs text-gray-500",children:"Click để chuyển câu"})]})]}),e("div",{className:"rounded-xl border border-gray-200 bg-gradient-to-b from-gray-50 to-white p-4 shadow-inner",children:e("div",{className:"grid max-h-[320px] grid-cols-6 gap-3 overflow-y-auto px-3 py-2 justify-center",children:u.map(t=>{const r=t+1,a=o(l,t),s=a?d(a,t):void 0,n=[`Câu ${r}`,a?.name||"Unknown Part"];return s&&n.push(s.name),e("button",{onClick:()=>h(t),className:g(t),title:n.join(" - "),children:r},t)})})}),t("div",{className:"rounded-xl border border-gray-100 bg-white p-3 shadow-sm",children:[e("div",{className:"mb-2 text-xs font-bold uppercase tracking-wide text-gray-400",children:"Chú thích"}),t("div",{className:"grid grid-cols-1 gap-2",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 shadow-md",children:e(n,{className:"h-3.5 w-3.5 text-white"})}),e("span",{className:"text-xs font-medium text-gray-700",children:"Câu đang làm"})]}),t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-green-400 to-emerald-500 shadow-sm",children:e(r,{className:"h-3.5 w-3.5 text-white"})}),e("span",{className:"text-xs font-medium text-gray-700",children:"Đã hoàn thành"})]}),t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-7 w-7 items-center justify-center rounded-lg border-2 border-gray-200 bg-white",children:e(a,{className:"h-3.5 w-3.5 text-gray-400"})}),e("span",{className:"text-xs font-medium text-gray-700",children:"Chưa tạo"})]})]})]})]})}
@@ -1,10 +1,13 @@
1
1
  import type { UserRole } from '../../shared/types/common.types';
2
2
  import type { ExamQuestionsSdkApi } from '../../api/exam-questions.types';
3
+ import type { CreateExamTexts } from './create-exam.texts';
3
4
  export interface ExamQuestionsPageContainerProps {
4
5
  examId: string;
5
6
  role?: UserRole.ADMIN | UserRole.TEACHER;
6
7
  api: ExamQuestionsSdkApi;
7
8
  listPath?: string;
8
9
  onNavigate?: (href: string) => void;
10
+ /** Optional label overrides for embedded contexts. */
11
+ texts?: CreateExamTexts;
9
12
  }
10
13
  export declare function ExamQuestionsPageContainer(props: ExamQuestionsPageContainerProps): import("react").JSX.Element;