@tinyweb_dev/oe-exam-sdk 0.1.1 → 0.1.2

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 (485) 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.js +1 -98
  4. package/dist/api/exam-questions.types.js +1 -1
  5. package/dist/api/exam-taking.js +1 -73
  6. package/dist/api/exam-taking.types.js +1 -1
  7. package/dist/api/index.js +1 -6
  8. package/dist/api/result-review.js +1 -50
  9. package/dist/api/result-review.types.js +1 -1
  10. package/dist/components/CueCardEditor.js +1 -58
  11. package/dist/components/common/ResolvedImage.js +1 -15
  12. package/dist/components/exams/CreateExamInfoForm.js +1 -35
  13. package/dist/components/exams/CreateExamPageContainer.js +1 -103
  14. package/dist/components/exams/ExamCreator.js +1 -89
  15. package/dist/components/exams/ExamPartTabs.js +1 -40
  16. package/dist/components/exams/ExamQuestionGrid.js +1 -18
  17. package/dist/components/exams/ExamQuestionsPageContainer.js +1 -90
  18. package/dist/components/exams/PartAudioUploader.js +1 -9
  19. package/dist/components/exams/TemplateSelector.js +1 -66
  20. package/dist/components/exams/api.js +1 -3
  21. package/dist/components/exams/create-exam.validation.js +1 -18
  22. package/dist/components/exams/exam-template.utils.js +1 -18
  23. package/dist/components/exams/index.js +1 -10
  24. package/dist/components/exams/take/ExamTakingPageContainer.js +1 -153
  25. package/dist/components/exams/take/GenericQuestionRenderer.js +1 -14
  26. package/dist/components/exams/take/exam-taking.utils.js +1 -88
  27. package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -24
  28. package/dist/components/exams/take/index.js +1 -2
  29. package/dist/components/exams/take/types.js +1 -1
  30. package/dist/components/index.js +1 -42
  31. package/dist/components/questions/_shared/config/question-types.config.js +1 -333
  32. package/dist/components/questions/_shared/constants/color-palette.js +1 -21
  33. package/dist/components/questions/_shared/hooks/useAnswerGroupQuestionGroup.js +1 -105
  34. package/dist/components/questions/_shared/hooks/useBasicQuestionGroup.js +1 -222
  35. package/dist/components/questions/_shared/hooks/useCompareImagesGroup.js +1 -140
  36. package/dist/components/questions/_shared/hooks/useImageQuestionGroup.js +1 -123
  37. package/dist/components/questions/_shared/hooks/useInfoExchangeGroup.js +1 -50
  38. package/dist/components/questions/_shared/hooks/useQuestionFormState.js +1 -54
  39. package/dist/components/questions/_shared/hooks/useQuestionInitialization.js +1 -123
  40. package/dist/components/questions/_shared/hooks/useQuestionListGroup.js +1 -121
  41. package/dist/components/questions/_shared/hooks/useQuestionOnChange.js +1 -70
  42. package/dist/components/questions/_shared/hooks/useSceneQuestionGroup.js +1 -194
  43. package/dist/components/questions/_shared/index.js +1 -7
  44. package/dist/components/questions/_shared/types/answer-the-question.type.js +1 -1
  45. package/dist/components/questions/_shared/types/arrange-letters-into-words.type.js +1 -1
  46. package/dist/components/questions/_shared/types/choose-correct-adjective.type.js +1 -3
  47. package/dist/components/questions/_shared/types/color-region.config.js +1 -1
  48. package/dist/components/questions/_shared/types/fill-in-blank.type.js +1 -1
  49. package/dist/components/questions/_shared/types/fill-missing-words-in-grid.type.js +1 -1
  50. package/dist/components/questions/_shared/types/gn-speaking-interview.type.js +1 -1
  51. package/dist/components/questions/_shared/types/index.js +1 -12
  52. package/dist/components/questions/_shared/types/label-the-picture.type.js +1 -1
  53. package/dist/components/questions/_shared/types/listen-and-choose-from-answer-group.type.js +1 -1
  54. package/dist/components/questions/_shared/types/listen-and-choose-objects-in-scene.type.js +1 -1
  55. package/dist/components/questions/_shared/types/listen-and-drag-objects-into-scene.type.js +1 -1
  56. package/dist/components/questions/_shared/types/listen-and-speak-answer.type.js +1 -1
  57. package/dist/components/questions/_shared/types/listen-and-speak-compare-images.type.js +1 -1
  58. package/dist/components/questions/_shared/types/listen-and-speak-image-group.type.js +1 -1
  59. package/dist/components/questions/_shared/types/listen-and-speak-info-exchange.type.js +1 -1
  60. package/dist/components/questions/_shared/types/listen-and-speak-odd-one-out.type.js +1 -3
  61. package/dist/components/questions/_shared/types/listen-and-speak-question-list.type.js +1 -1
  62. package/dist/components/questions/_shared/types/listen-and-speak-with-story-images.type.js +1 -3
  63. package/dist/components/questions/_shared/types/listen-and-tick-answer.type.js +1 -1
  64. package/dist/components/questions/_shared/types/listening-drawline.type.js +1 -1
  65. package/dist/components/questions/_shared/types/look-and-choose-answer.type.js +1 -1
  66. package/dist/components/questions/_shared/types/look-picture-fill-word-hint.type.js +1 -1
  67. package/dist/components/questions/_shared/types/match-by-writing-answer.type.js +1 -1
  68. package/dist/components/questions/_shared/types/multiple-choice.type.js +1 -1
  69. package/dist/components/questions/_shared/types/question-group.type.js +1 -11
  70. package/dist/components/questions/_shared/types/read-and-color-objects.type.js +1 -1
  71. package/dist/components/questions/_shared/types/read-displayed-content.type.js +1 -1
  72. package/dist/components/questions/_shared/types/read-passage-and-answer-questions.type.js +1 -1
  73. package/dist/components/questions/_shared/types/speaking-describe-image.type.js +1 -1
  74. package/dist/components/questions/_shared/types/spontaneous-qa.type.js +1 -1
  75. package/dist/components/questions/_shared/types/word-fill-paragraph-with-options.type.js +1 -1
  76. package/dist/components/questions/_shared/types/word-fill-paragraph.type.js +1 -1
  77. package/dist/components/questions/_shared/types/word-fill-structured-form.type.js +1 -1
  78. package/dist/components/questions/_shared/types/word-ordering.type.js +1 -1
  79. package/dist/components/questions/_shared/types/write-a-short-letter.type.js +1 -1
  80. package/dist/components/questions/_shared/types/write-correct-verb-form.type.js +1 -3
  81. package/dist/components/questions/_shared/types/write-sentences.type.js +1 -1
  82. package/dist/components/questions/_shared/types/write-short-paragraph.type.js +1 -1
  83. package/dist/components/questions/_shared/utils/answer-builder.js +1 -79
  84. package/dist/components/questions/_shared/utils/createColorRegionRegistration.js +1 -39
  85. package/dist/components/questions/_shared/utils/id-generator.js +1 -3
  86. package/dist/components/questions/_shared/utils/question-validation.js +1 -21
  87. package/dist/components/questions/components/ColorNode.js +1 -85
  88. package/dist/components/questions/components/ColorRegionClient.js +1 -545
  89. package/dist/components/questions/components/ColorRegionCreator.js +1 -420
  90. package/dist/components/questions/components/ColorRegionNode.js +1 -59
  91. package/dist/components/questions/components/CreatorGuide.js +1 -103
  92. package/dist/components/questions/components/NameNode.js +1 -26
  93. package/dist/components/questions/components/QuestionBankOption.js +1 -46
  94. package/dist/components/questions/components/QuestionBasicInfoForm.js +1 -9
  95. package/dist/components/questions/components/QuestionCommonFields.js +1 -33
  96. package/dist/components/questions/components/QuestionSearchDropdown.js +1 -258
  97. package/dist/components/questions/components/QuestionTypeSelector.js +1 -32
  98. package/dist/components/questions/components/QuestionTypeSpecificForms.js +1 -6
  99. package/dist/components/questions/components/RegionNode.js +1 -94
  100. package/dist/components/questions/components/index.js +1 -14
  101. package/dist/components/questions/creator/QuestionCreator.js +1 -90
  102. package/dist/components/questions/creator/QuestionGroupCreator.js +1 -119
  103. package/dist/components/questions/creator/dedicated-component-router.js +1 -79
  104. package/dist/components/questions/creator/index.js +1 -3
  105. package/dist/components/questions/creator/question-type-registry.js +1 -80
  106. package/dist/components/questions/groups/ArticlesGroupCard.js +1 -121
  107. package/dist/components/questions/groups/BasicQuestionGroupCard.js +1 -50
  108. package/dist/components/questions/groups/DocumentGroupCard.js +1 -114
  109. package/dist/components/questions/groups/MatchAnswerGroupCard.js +1 -25
  110. package/dist/components/questions/index.js +1 -10
  111. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +1 -37
  112. package/dist/components/questions/types/answer-the-question/AnswerTheQuestionCreator.js +1 -351
  113. package/dist/components/questions/types/answer-the-question/register.js +1 -67
  114. package/dist/components/questions/types/answer-the-question/transform.js +1 -36
  115. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsClient.js +1 -141
  116. package/dist/components/questions/types/arrange-letters-into-words/ArrangeLettersIntoWordsCreator.js +1 -118
  117. package/dist/components/questions/types/arrange-letters-into-words/index.js +1 -2
  118. package/dist/components/questions/types/arrange-letters-into-words/register.js +1 -4
  119. package/dist/components/questions/types/arrange-letters-into-words/transform.js +1 -24
  120. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveClient.js +1 -64
  121. package/dist/components/questions/types/choose-correct-adjective/ChooseCorrectAdjectiveCreator.js +1 -169
  122. package/dist/components/questions/types/choose-correct-adjective/index.js +1 -2
  123. package/dist/components/questions/types/choose-correct-adjective/register.js +1 -55
  124. package/dist/components/questions/types/choose-correct-adjective/transform.js +1 -16
  125. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerClient.js +1 -131
  126. package/dist/components/questions/types/choose-the-correct-answer/ChooseTheCorrectAnswerCreator.js +1 -581
  127. package/dist/components/questions/types/choose-the-correct-answer/index.js +1 -2
  128. package/dist/components/questions/types/choose-the-correct-answer/register.js +1 -101
  129. package/dist/components/questions/types/choose-the-correct-answer/transform.js +1 -43
  130. package/dist/components/questions/types/fill-in-blank/FillInBlankClient.js +1 -275
  131. package/dist/components/questions/types/fill-in-blank/FillInBlankCreator.js +1 -448
  132. package/dist/components/questions/types/fill-in-blank/index.js +1 -2
  133. package/dist/components/questions/types/fill-in-blank/register.js +1 -138
  134. package/dist/components/questions/types/fill-in-blank/transform.js +1 -59
  135. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridClient.js +1 -117
  136. package/dist/components/questions/types/fill-missing-words-in-grid/FillMissingWordsInGridCreator.js +1 -671
  137. package/dist/components/questions/types/fill-missing-words-in-grid/index.js +1 -2
  138. package/dist/components/questions/types/fill-missing-words-in-grid/register.js +1 -37
  139. package/dist/components/questions/types/fill-missing-words-in-grid/transform.js +1 -15
  140. package/dist/components/questions/types/gn-speaking-interview/GNSpeakingInterviewCreator.js +1 -262
  141. package/dist/components/questions/types/gn-speaking-interview/register.js +1 -121
  142. package/dist/components/questions/types/gn-speaking-interview/transform.js +1 -48
  143. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingClient.js +1 -16
  144. package/dist/components/questions/types/image-object-matching/ImageObjectMatchingCreator.js +1 -27
  145. package/dist/components/questions/types/image-object-matching/index.js +1 -2
  146. package/dist/components/questions/types/image-object-matching/register.js +1 -3
  147. package/dist/components/questions/types/image-object-matching/transform.js +1 -2
  148. package/dist/components/questions/types/label-the-picture/LabelThePictureClient.js +1 -102
  149. package/dist/components/questions/types/label-the-picture/LabelThePictureCreator.js +1 -344
  150. package/dist/components/questions/types/label-the-picture/index.js +1 -2
  151. package/dist/components/questions/types/label-the-picture/register.js +1 -54
  152. package/dist/components/questions/types/label-the-picture/transform.js +1 -28
  153. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupClient.js +1 -78
  154. package/dist/components/questions/types/listen-and-choose-from-answer-group/ListenAndChooseFromAnswerGroupCreator.js +1 -232
  155. package/dist/components/questions/types/listen-and-choose-from-answer-group/index.js +1 -2
  156. package/dist/components/questions/types/listen-and-choose-from-answer-group/register.js +1 -82
  157. package/dist/components/questions/types/listen-and-choose-from-answer-group/transform.js +1 -27
  158. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneClient.js +1 -70
  159. package/dist/components/questions/types/listen-and-choose-objects-in-scene/ListenAndChooseObjectsInSceneCreator.js +1 -386
  160. package/dist/components/questions/types/listen-and-choose-objects-in-scene/index.js +1 -2
  161. package/dist/components/questions/types/listen-and-choose-objects-in-scene/register.js +1 -100
  162. package/dist/components/questions/types/listen-and-choose-objects-in-scene/transform.js +1 -33
  163. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneClient.js +1 -106
  164. package/dist/components/questions/types/listen-and-drag-objects-into-scene/ListenAndDragObjectsIntoSceneCreator.js +1 -457
  165. package/dist/components/questions/types/listen-and-drag-objects-into-scene/index.js +1 -2
  166. package/dist/components/questions/types/listen-and-drag-objects-into-scene/register.js +1 -110
  167. package/dist/components/questions/types/listen-and-drag-objects-into-scene/transform.js +1 -35
  168. package/dist/components/questions/types/listen-and-speak-answer/ArrowIndicator.js +1 -8
  169. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerClient.js +1 -22
  170. package/dist/components/questions/types/listen-and-speak-answer/ListenAndSpeakAnswerCreator.js +1 -291
  171. package/dist/components/questions/types/listen-and-speak-answer/index.js +1 -2
  172. package/dist/components/questions/types/listen-and-speak-answer/register.js +1 -84
  173. package/dist/components/questions/types/listen-and-speak-answer/transform.js +1 -27
  174. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesClient.js +1 -32
  175. package/dist/components/questions/types/listen-and-speak-compare-images/ListenAndSpeakCompareImagesCreator.js +1 -295
  176. package/dist/components/questions/types/listen-and-speak-compare-images/register.js +1 -79
  177. package/dist/components/questions/types/listen-and-speak-compare-images/transform.js +1 -27
  178. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupClient.js +1 -34
  179. package/dist/components/questions/types/listen-and-speak-image-group/ListenAndSpeakImageGroupCreator.js +1 -285
  180. package/dist/components/questions/types/listen-and-speak-image-group/register.js +1 -81
  181. package/dist/components/questions/types/listen-and-speak-image-group/transform.js +1 -26
  182. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeClient.js +1 -38
  183. package/dist/components/questions/types/listen-and-speak-info-exchange/ListenAndSpeakInfoExchangeCreator.js +1 -385
  184. package/dist/components/questions/types/listen-and-speak-info-exchange/register.js +1 -87
  185. package/dist/components/questions/types/listen-and-speak-info-exchange/transform.js +1 -32
  186. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutClient.js +1 -26
  187. package/dist/components/questions/types/listen-and-speak-odd-one-out/ListenAndSpeakOddOneOutCreator.js +1 -247
  188. package/dist/components/questions/types/listen-and-speak-odd-one-out/register.js +1 -59
  189. package/dist/components/questions/types/listen-and-speak-odd-one-out/transform.js +1 -18
  190. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListClient.js +1 -26
  191. package/dist/components/questions/types/listen-and-speak-question-list/ListenAndSpeakQuestionListCreator.js +1 -256
  192. package/dist/components/questions/types/listen-and-speak-question-list/register.js +1 -81
  193. package/dist/components/questions/types/listen-and-speak-question-list/transform.js +1 -26
  194. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesClient.js +1 -26
  195. package/dist/components/questions/types/listen-and-speak-with-story-images/ListenAndSpeakWithStoryImagesCreator.js +1 -230
  196. package/dist/components/questions/types/listen-and-speak-with-story-images/register.js +1 -56
  197. package/dist/components/questions/types/listen-and-speak-with-story-images/transform.js +1 -16
  198. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerClient.js +1 -72
  199. package/dist/components/questions/types/look-picture-choose-correct-answer/LookPictureChooseCorrectAnswerCreator.js +1 -230
  200. package/dist/components/questions/types/look-picture-choose-correct-answer/index.js +1 -1
  201. package/dist/components/questions/types/look-picture-choose-correct-answer/register.js +1 -66
  202. package/dist/components/questions/types/look-picture-choose-correct-answer/transform.js +1 -42
  203. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerClient.js +1 -96
  204. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/LookPictureFillBlankChooseAnswerCreator.js +1 -238
  205. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/index.js +1 -2
  206. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/register.js +1 -76
  207. package/dist/components/questions/types/look-picture-fill-blank-choose-answer/transform.js +1 -19
  208. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintClient.js +1 -134
  209. package/dist/components/questions/types/look-picture-fill-word-hint/LookPictureFillWordHintCreator.js +1 -617
  210. package/dist/components/questions/types/look-picture-fill-word-hint/index.js +1 -2
  211. package/dist/components/questions/types/look-picture-fill-word-hint/register.js +1 -82
  212. package/dist/components/questions/types/look-picture-fill-word-hint/transform.js +1 -29
  213. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerClient.js +1 -38
  214. package/dist/components/questions/types/match-by-writing-answer/MatchByWritingAnswerCreator.js +1 -359
  215. package/dist/components/questions/types/match-by-writing-answer/index.js +1 -3
  216. package/dist/components/questions/types/match-by-writing-answer/register.js +1 -58
  217. package/dist/components/questions/types/match-by-writing-answer/transform.js +1 -61
  218. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsClient.js +1 -16
  219. package/dist/components/questions/types/read-and-color-objects/ReadAndColorObjectsCreator.js +1 -23
  220. package/dist/components/questions/types/read-and-color-objects/index.js +1 -2
  221. package/dist/components/questions/types/read-and-color-objects/register.js +1 -3
  222. package/dist/components/questions/types/read-and-color-objects/transform.js +1 -36
  223. package/dist/components/questions/types/read-displayed-content/ReadDisplayedContentCreator.js +1 -125
  224. package/dist/components/questions/types/read-displayed-content/register.js +1 -64
  225. package/dist/components/questions/types/read-displayed-content/transform.js +1 -12
  226. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsClient.js +1 -64
  227. package/dist/components/questions/types/read-passage-and-answer-questions/ReadPassageAndAnswerQuestionsCreator.js +1 -511
  228. package/dist/components/questions/types/read-passage-and-answer-questions/index.js +1 -2
  229. package/dist/components/questions/types/read-passage-and-answer-questions/register.js +1 -58
  230. package/dist/components/questions/types/read-passage-and-answer-questions/transform.js +1 -28
  231. package/dist/components/questions/types/speaking-conversation/SpeakingConversationCreator.js +1 -373
  232. package/dist/components/questions/types/speaking-conversation/SpeakingConversationPreviewClient.js +1 -41
  233. package/dist/components/questions/types/speaking-conversation/register.js +1 -110
  234. package/dist/components/questions/types/speaking-conversation/transform.js +1 -34
  235. package/dist/components/questions/types/speaking-cue-card/SpeakingCueCardCreator.js +1 -74
  236. package/dist/components/questions/types/speaking-cue-card/register.js +1 -52
  237. package/dist/components/questions/types/speaking-cue-card/transform.js +1 -12
  238. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageClient.js +1 -10
  239. package/dist/components/questions/types/speaking-describe-image/SpeakingDescribeImageCreator.js +1 -161
  240. package/dist/components/questions/types/speaking-describe-image/index.js +1 -2
  241. package/dist/components/questions/types/speaking-describe-image/register.js +1 -69
  242. package/dist/components/questions/types/speaking-describe-image/transform.js +1 -16
  243. package/dist/components/questions/types/spontaneous-qa/SpontaneousQACreator.js +1 -234
  244. package/dist/components/questions/types/spontaneous-qa/register.js +1 -93
  245. package/dist/components/questions/types/spontaneous-qa/transform.js +1 -34
  246. package/dist/components/questions/types/true-false/TrueFalseClient.js +1 -75
  247. package/dist/components/questions/types/true-false/TrueFalseCreator.js +1 -244
  248. package/dist/components/questions/types/true-false/index.js +1 -2
  249. package/dist/components/questions/types/true-false/register.js +1 -77
  250. package/dist/components/questions/types/true-false/transform.js +1 -32
  251. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphClient.js +1 -233
  252. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphCreator.js +1 -631
  253. package/dist/components/questions/types/word-fill-paragraph/WordFillParagraphWrapper.js +1 -28
  254. package/dist/components/questions/types/word-fill-paragraph/index.js +1 -2
  255. package/dist/components/questions/types/word-fill-paragraph/register.js +1 -136
  256. package/dist/components/questions/types/word-fill-paragraph/transform.js +1 -56
  257. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormClient.js +1 -138
  258. package/dist/components/questions/types/word-fill-structured-form/WordFillStructuredFormCreator.js +1 -261
  259. package/dist/components/questions/types/word-fill-structured-form/index.js +1 -2
  260. package/dist/components/questions/types/word-fill-structured-form/register.js +1 -67
  261. package/dist/components/questions/types/word-fill-structured-form/transform.js +1 -40
  262. package/dist/components/questions/types/word-ordering/WordOrderingClient.js +1 -84
  263. package/dist/components/questions/types/word-ordering/WordOrderingCreator.js +1 -192
  264. package/dist/components/questions/types/word-ordering/index.js +1 -2
  265. package/dist/components/questions/types/word-ordering/register.js +1 -46
  266. package/dist/components/questions/types/word-ordering/transform.js +1 -24
  267. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterClient.js +1 -98
  268. package/dist/components/questions/types/write-a-short-letter/WriteAShortLetterCreator.js +1 -320
  269. package/dist/components/questions/types/write-a-short-letter/index.js +1 -2
  270. package/dist/components/questions/types/write-a-short-letter/register.js +1 -68
  271. package/dist/components/questions/types/write-a-short-letter/transform.js +1 -25
  272. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormClient.js +1 -61
  273. package/dist/components/questions/types/write-correct-verb-form/WriteCorrectVerbFormCreator.js +1 -196
  274. package/dist/components/questions/types/write-correct-verb-form/index.js +1 -2
  275. package/dist/components/questions/types/write-correct-verb-form/register.js +1 -62
  276. package/dist/components/questions/types/write-correct-verb-form/transform.js +1 -17
  277. package/dist/components/questions/types/write-sentences/WriteSentencesClient.js +1 -22
  278. package/dist/components/questions/types/write-sentences/WriteSentencesCreator.js +1 -174
  279. package/dist/components/questions/types/write-sentences/index.js +1 -2
  280. package/dist/components/questions/types/write-sentences/register.js +1 -58
  281. package/dist/components/questions/types/write-sentences/transform.js +1 -28
  282. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphClient.js +1 -96
  283. package/dist/components/questions/types/write-short-paragraph/WriteShortParagraphCreator.js +1 -161
  284. package/dist/components/questions/types/write-short-paragraph/index.js +1 -2
  285. package/dist/components/questions/types/write-short-paragraph/register.js +1 -4
  286. package/dist/components/questions/types/write-short-paragraph/transform.js +1 -21
  287. package/dist/components/questions/viewer/QuestionGroupViewer.js +1 -36
  288. package/dist/components/questions/viewer/QuestionViewer.js +1 -446
  289. package/dist/components/questions/viewer/index.js +1 -2
  290. package/dist/components/results/ResultReviewPageContainer.js +1 -98
  291. package/dist/components/results/ReviewQuestionRenderer.js +1 -72
  292. package/dist/components/results/index.js +1 -2
  293. package/dist/components/results/result-review.utils.js +1 -6
  294. package/dist/components/results/review-data.js +1 -150
  295. package/dist/components/results/review-renderer.utils.js +1 -97
  296. package/dist/components/results/review-types.js +1 -1
  297. package/dist/components/themes/cambridge-yle/CambridgeYleAudioPlayer.js +1 -138
  298. package/dist/components/themes/cambridge-yle/CambridgeYleBottomNav.js +1 -12
  299. package/dist/components/themes/cambridge-yle/CambridgeYleExamLayout.js +1 -18
  300. package/dist/components/themes/cambridge-yle/CambridgeYleExampleText.js +1 -11
  301. package/dist/components/themes/cambridge-yle/CambridgeYleHeader.js +1 -11
  302. package/dist/components/themes/cambridge-yle/CambridgeYleInstructionBanner.js +1 -9
  303. package/dist/components/themes/cambridge-yle/CambridgeYleNavButton.js +1 -13
  304. package/dist/components/themes/cambridge-yle/CambridgeYlePartBanner.js +1 -8
  305. package/dist/components/themes/cambridge-yle/CambridgeYleProgressBar.js +1 -6
  306. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCard.js +1 -5
  307. package/dist/components/themes/cambridge-yle/CambridgeYleQuestionCardFullWidth.js +1 -9
  308. package/dist/components/themes/cambridge-yle/CambridgeYleSidebar.js +1 -41
  309. package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -25
  310. package/dist/components/themes/cambridge-yle/index.js +1 -13
  311. package/dist/components/themes/english-certification/EcAnswerTheQuestion.js +1 -24
  312. package/dist/components/themes/english-certification/EcMatchByWritingAnswer.js +1 -88
  313. package/dist/components/themes/english-certification/EcWordFillParagraph.js +1 -55
  314. package/dist/components/themes/english-certification/EcWordFillStructuredForm.js +1 -156
  315. package/dist/components/themes/english-certification/EcWritingTask.js +1 -34
  316. package/dist/components/themes/english-certification/EnglishCertificationAudioPlayer.js +1 -114
  317. package/dist/components/themes/english-certification/EnglishCertificationBrand.js +1 -11
  318. package/dist/components/themes/english-certification/EnglishCertificationExamLayout.js +1 -21
  319. package/dist/components/themes/english-certification/EnglishCertificationFlagButton.js +1 -15
  320. package/dist/components/themes/english-certification/EnglishCertificationFooterBar.js +1 -19
  321. package/dist/components/themes/english-certification/EnglishCertificationGroupedQuestion.js +1 -148
  322. package/dist/components/themes/english-certification/EnglishCertificationHeader.js +1 -31
  323. package/dist/components/themes/english-certification/EnglishCertificationNavigationPanel.js +1 -71
  324. package/dist/components/themes/english-certification/EnglishCertificationOptionSelector.js +1 -33
  325. package/dist/components/themes/english-certification/EnglishCertificationPassagePanel.js +1 -16
  326. package/dist/components/themes/english-certification/EnglishCertificationQuestionCard.js +1 -22
  327. package/dist/components/themes/english-certification/EnglishCertificationQuestionItem.js +1 -37
  328. package/dist/components/themes/english-certification/EnglishCertificationQuestionRenderer.js +1 -72
  329. package/dist/components/themes/english-certification/EnglishCertificationReadingSection.js +1 -90
  330. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestion.js +1 -18
  331. package/dist/components/themes/english-certification/EnglishCertificationSingleQuestionList.js +1 -29
  332. package/dist/components/themes/english-certification/english-certification-theme.js +1 -58
  333. package/dist/components/themes/english-certification/grouped-question-utils.js +1 -218
  334. package/dist/components/themes/english-certification/index.js +1 -21
  335. package/dist/components/themes/index.js +1 -6
  336. package/dist/components/themes/summer-sky/SummerSkyAudioPlayer.js +1 -170
  337. package/dist/components/themes/summer-sky/SummerSkyLayout.js +1 -221
  338. package/dist/components/themes/summer-sky/SummerSkySubmitModal.js +1 -18
  339. package/dist/components/themes/summer-sky/SummerSkyWaitingRoom.js +1 -194
  340. package/dist/components/themes/types.js +1 -1
  341. package/dist/components/ui/alert-dialog.js +1 -26
  342. package/dist/components/ui/alert.js +1 -22
  343. package/dist/components/ui/avatar.js +1 -12
  344. package/dist/components/ui/badge.js +1 -38
  345. package/dist/components/ui/button.js +1 -35
  346. package/dist/components/ui/calendar.js +1 -76
  347. package/dist/components/ui/card.js +1 -16
  348. package/dist/components/ui/carousel.js +1 -91
  349. package/dist/components/ui/checkbox.js +1 -9
  350. package/dist/components/ui/collapsible.js +1 -13
  351. package/dist/components/ui/command.js +1 -34
  352. package/dist/components/ui/confirm-dialog.js +1 -49
  353. package/dist/components/ui/dialog.js +1 -23
  354. package/dist/components/ui/drawer.js +1 -35
  355. package/dist/components/ui/dropdown-menu.js +1 -33
  356. package/dist/components/ui/file-upload.js +1 -155
  357. package/dist/components/ui/form.js +1 -62
  358. package/dist/components/ui/hover-card.js +1 -10
  359. package/dist/components/ui/image-preview.js +1 -29
  360. package/dist/components/ui/input.js +1 -8
  361. package/dist/components/ui/label.js +1 -9
  362. package/dist/components/ui/language-switcher.js +1 -15
  363. package/dist/components/ui/multi-select.js +1 -90
  364. package/dist/components/ui/points-input.js +1 -33
  365. package/dist/components/ui/popover.js +1 -26
  366. package/dist/components/ui/progress.js +1 -16
  367. package/dist/components/ui/radio-group.js +1 -12
  368. package/dist/components/ui/scroll-area.js +1 -11
  369. package/dist/components/ui/select.js +1 -38
  370. package/dist/components/ui/separator.js +1 -8
  371. package/dist/components/ui/sheet.js +1 -40
  372. package/dist/components/ui/single-select.js +1 -76
  373. package/dist/components/ui/skeleton.js +1 -6
  374. package/dist/components/ui/switch.js +1 -8
  375. package/dist/components/ui/table.js +1 -20
  376. package/dist/components/ui/tabs.js +1 -17
  377. package/dist/components/ui/textarea.js +1 -8
  378. package/dist/components/ui/tooltip.js +1 -11
  379. package/dist/index.js +1 -19
  380. package/dist/mcp/config.js +1 -32
  381. package/dist/mcp/http-client.js +1 -48
  382. package/dist/mcp/index.js +1 -29
  383. package/dist/mcp/tools/_helpers.js +1 -25
  384. package/dist/mcp/tools/exams.js +1 -439
  385. package/dist/mcp/tools/index.js +1 -6
  386. package/dist/mcp/tools/results.js +1 -19
  387. package/dist/shared/constants/ApiConstant.js +1 -276
  388. package/dist/shared/constants/QueryKeyConstant.js +1 -112
  389. package/dist/shared/constants/exam-level.enum.js +1 -41
  390. package/dist/shared/constants/landings/placement-test.constants.js +1 -431
  391. package/dist/shared/constants/question-skills.js +1 -267
  392. package/dist/shared/constants/routes.js +1 -81
  393. package/dist/shared/index.js +1 -7
  394. package/dist/shared/lib/hooks/index.js +1 -10
  395. package/dist/shared/lib/hooks/useAlertDialog.js +1 -57
  396. package/dist/shared/lib/hooks/useAudioPlayCounter.js +1 -30
  397. package/dist/shared/lib/hooks/useAudioPreview.js +1 -81
  398. package/dist/shared/lib/hooks/useAudioUpload.js +1 -24
  399. package/dist/shared/lib/hooks/useConfirmDialog.js +1 -89
  400. package/dist/shared/lib/hooks/useDebouncedCallback.js +1 -32
  401. package/dist/shared/lib/hooks/useDelayedAutoPlay.js +1 -12
  402. package/dist/shared/lib/hooks/useFileUpload.js +1 -24
  403. package/dist/shared/lib/hooks/useImageUrl.js +1 -5
  404. package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -10
  405. package/dist/shared/lib/hooks/useReactHookForm.js +1 -31
  406. package/dist/shared/lib/hooks/useToast.js +1 -62
  407. package/dist/shared/lib/i18n/index.js +1 -54
  408. package/dist/shared/lib/i18n/messages/en/admin.js +1 -2104
  409. package/dist/shared/lib/i18n/messages/en/auth.js +1 -86
  410. package/dist/shared/lib/i18n/messages/en/common.js +1 -140
  411. package/dist/shared/lib/i18n/messages/en/file-upload.js +1 -18
  412. package/dist/shared/lib/i18n/messages/en/student.js +1 -133
  413. package/dist/shared/lib/i18n/messages/en/terms.js +1 -37
  414. package/dist/shared/lib/i18n/messages/en.js +1 -15
  415. package/dist/shared/lib/i18n/messages/vi/admin.js +1 -2104
  416. package/dist/shared/lib/i18n/messages/vi/auth.js +1 -86
  417. package/dist/shared/lib/i18n/messages/vi/common.js +1 -140
  418. package/dist/shared/lib/i18n/messages/vi/file-upload.js +1 -18
  419. package/dist/shared/lib/i18n/messages/vi/student.js +1 -133
  420. package/dist/shared/lib/i18n/messages/vi/terms.js +1 -37
  421. package/dist/shared/lib/i18n/messages/vi.js +1 -15
  422. package/dist/shared/lib/i18n/types.js +1 -1
  423. package/dist/shared/lib/stores/examQuestionStore.js +1 -212
  424. package/dist/shared/lib/stores/localeStore.js +1 -10
  425. package/dist/shared/lib/utils/public-page.js +1 -44
  426. package/dist/shared/lib/utils/question-transform-types.js +1 -1
  427. package/dist/shared/lib/utils/question-transform.js +1 -174
  428. package/dist/shared/lib/utils.js +1 -168
  429. package/dist/shared/lib/validation/choose-correct-answer.validation.js +1 -111
  430. package/dist/shared/lib/validation/label-the-picture.validation.js +1 -19
  431. package/dist/shared/lib/validation/listen-and-choose-from-answer-group.validation.js +1 -46
  432. package/dist/shared/lib/validation/listen-and-choose-objects-in-scene.validation.js +1 -21
  433. package/dist/shared/lib/validation/listen-and-drag-objects-into-scene.validation.js +1 -34
  434. package/dist/shared/lib/validation/listen-and-speak-answer.validation.js +1 -40
  435. package/dist/shared/lib/validation/listen-and-speak-compare-images.validation.js +1 -45
  436. package/dist/shared/lib/validation/listen-and-speak-image-group.validation.js +1 -19
  437. package/dist/shared/lib/validation/listen-and-speak-info-exchange.validation.js +1 -39
  438. package/dist/shared/lib/validation/listen-and-speak-odd-one-out.validation.js +1 -32
  439. package/dist/shared/lib/validation/listen-and-speak-question-list.validation.js +1 -19
  440. package/dist/shared/lib/validation/listen-and-speak-with-story-images.validation.js +1 -28
  441. package/dist/shared/lib/validation/look-picture-choose-correct-answer.validation.js +1 -12
  442. package/dist/shared/lib/validation/look-picture-fill-blank-choose-answer.validation.js +1 -47
  443. package/dist/shared/lib/validation/read-and-color-objects.validation.js +1 -29
  444. package/dist/shared/lib/validation/read-displayed-content.validation.js +1 -28
  445. package/dist/shared/lib/validation/speaking-conversation.validation.js +1 -75
  446. package/dist/shared/lib/validation/speaking-describe-image.validation.js +1 -31
  447. package/dist/shared/lib/validation/spontaneous-qa.validation.js +1 -48
  448. package/dist/shared/lib/validation/word-fill-paragraph-default.validation.js +1 -38
  449. package/dist/shared/lib/validation/word-fill-paragraph-with-options.validation.js +1 -39
  450. package/dist/shared/lib/validation/word-fill-paragraph.validation.js +1 -43
  451. package/dist/shared/lib/validation/word-ordering.validation.js +1 -33
  452. package/dist/shared/types/branch.types.js +1 -6
  453. package/dist/shared/types/common.types.js +1 -50
  454. package/dist/shared/types/entities/exam-schedule.types.js +1 -5
  455. package/dist/shared/types/entities/exam.types.js +1 -1
  456. package/dist/shared/types/entities/question.types.js +1 -4
  457. package/dist/shared/types/entities/result.types.js +1 -4
  458. package/dist/shared/types/entities/user.types.js +1 -4
  459. package/dist/shared/types/exam-taking.types.js +1 -2
  460. package/dist/shared/types/history.types.js +1 -34
  461. package/dist/shared/types/index.js +1 -1
  462. package/dist/shared/types/questions/choose-correct-adjective.js +1 -4
  463. package/dist/shared/types/questions/choose-the-correct-answer.js +1 -9
  464. package/dist/shared/types/questions/fill-missing-words-in-grid.js +1 -11
  465. package/dist/shared/types/questions/index.js +1 -40
  466. package/dist/shared/types/questions/listen-and-speak-answer.js +1 -1
  467. package/dist/shared/types/questions/listen-and-speak-compare-images.js +1 -1
  468. package/dist/shared/types/questions/listen-and-speak-image-group.js +1 -1
  469. package/dist/shared/types/questions/listen-and-speak-info-exchange.js +1 -3
  470. package/dist/shared/types/questions/listen-and-speak-odd-one-out.js +1 -3
  471. package/dist/shared/types/questions/listen-and-speak-question-list.js +1 -1
  472. package/dist/shared/types/questions/listen-and-speak-with-story-images.js +1 -3
  473. package/dist/shared/types/questions/look-picture-choose-correct-answer.js +1 -5
  474. package/dist/shared/types/questions/look-picture-fill-blank-choose-answer.js +1 -9
  475. package/dist/shared/types/questions/look-picture-fill-word-hint.js +1 -7
  476. package/dist/shared/types/questions/read-displayed-content.js +1 -1
  477. package/dist/shared/types/questions/speaking-conversation.js +1 -7
  478. package/dist/shared/types/questions/speaking-describe-image.js +1 -1
  479. package/dist/shared/types/questions/spontaneous-qa.js +1 -1
  480. package/dist/shared/types/questions/write-correct-verb-form.js +1 -12
  481. package/dist/shared/types/role.types.js +1 -1
  482. package/dist/shared/types/structure-management.js +1 -28
  483. package/dist/shared/types/student.types.js +1 -54
  484. package/dist/shared/types/teacher-dashboard-rooms.js +1 -2
  485. package/package.json +4 -2
@@ -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 a,Fragment as t}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 o}from"react-hook-form";import{ArrowLeft as l,ArrowRight as m,LayoutTemplate as c,Loader2 as d,X as h}from"lucide-react";import{Button as p}from"../../components/ui/button";import{Card as u,CardContent as f,CardHeader as x,CardTitle as g}from"../../components/ui/card";import{Dialog as v,DialogContent as b,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 k}from"../../shared/lib/i18n";import{buildExamTemplatePayload as E}from"../../api/exam-create";import{CreateExamInfoForm as I}from"./CreateExamInfoForm";import{TemplateSelector as T}from"./TemplateSelector";import{examFormSchema as j}from"./create-exam.validation";function L({api:n,listPath:c="/manage/exams",onNavigate:h}){const u=k(),{success:f,warning:x,error:g}=C(),[L,z]=r(!1),[D,P]=r(null),[q,M]=r([]),[O,F]=r(!1),[V,$]=r(!1),B=o({resolver:s(j),mode:"onBlur",defaultValues:{code:"",name:"",level:"",difficulty:"",duration:"",totalPoints:"100",description:""}});i(()=>{if(!n.getExamLevels)return;let e=!1;return F(!0),n.getExamLevels().then(a=>{e||M(a)}).catch(e=>g({message:u("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})).finally(()=>{e||F(!1)}),()=>{e=!0}},[n,g,u]);const H=e=>{h?h(e):"undefined"!=typeof window&&(window.location.href=e)},Q=B.handleSubmit(async e=>{if(D){$(!0);try{const t=function(e){return"object"==typeof e&&null!==e&&"data"in e}(a=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:D.id}))?a.data:a;await n.updateExamTemplate({examId:t.id,template:E(D)}),f({message:u("admin.exams.create.success"),description:u("admin.exams.create.successDesc",{name:t.name})}),H(`${c}/${t.id}/questions`)}catch(e){g({message:u("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})}finally{$(!1)}var a}else x({message:u("admin.exams.create.incompleteInfo"),description:"Vui lòng chọn template đề thi trước khi tiếp tục."})},()=>x({message:u("admin.exams.create.incompleteInfo"),description:u("admin.exams.create.incompleteInfoDesc")}));return a("div",{className:"space-y-6",children:[a("div",{className:"flex items-center gap-4",children:[e(p,{variant:"ghost",size:"icon",onClick:()=>H(c),className:"h-8 w-8",children:e(l,{className:"h-4 w-4"})}),a("div",{children:[e("h1",{className:"text-3xl font-bold tracking-tight text-gray-900",children:u("admin.exams.create.title")}),e("p",{className:"mt-1 text-gray-600",children:u("admin.exams.create.step1")})]})]}),e("form",{id:"exam-form",onSubmit:Q,children:e(I,{form:B,examLevels:q,isLoadingExamLevels:O})}),e(S,{selectedTemplate:D,onOpen:()=>z(!0),onClear:()=>P(null)}),e(v,{open:L,onOpenChange:z,children:a(b,{className:"flex max-h-[92vh] min-h-[92vh] max-w-5xl flex-col overflow-hidden",children:[a(y,{children:[e(w,{children:"Chọn template đề thi"}),e(N,{children:"Chọn loại đề thi bạn muốn tạo."})]}),e(T,{api:n,onTemplateSelect:e=>{P(e),z(!1)},onCancel:()=>z(!1)})]})}),a("div",{className:"flex justify-end gap-3",children:[e(p,{variant:"outline",onClick:()=>H(c),disabled:V,children:u("admin.exams.create.cancel")}),e(p,{type:"submit",form:"exam-form",className:"bg-blue-600 hover:bg-blue-700",disabled:V||!D,children:a(t,V?{children:[e(d,{className:"mr-2 h-4 w-4 animate-spin"}),u("admin.exams.create.saving")]}:{children:[u("admin.exams.create.saveAndContinue"),e(m,{className:"ml-2 h-4 w-4"})]})})]})]})}export function CreateExamPageContainer(a){return e(n,{fallback:e(z,{}),children:e(L,{...a})})}function S({selectedTemplate:t,onOpen:n,onClear:i}){return a(u,{className:"border border-gray-200",children:[e(x,{className:"border-b border-gray-100 bg-gray-50/50",children:a(g,{className:"flex items-center gap-2 text-base font-semibold",children:[e(c,{className:"h-4 w-4 text-blue-600"}),"Chọn Template ",e("span",{className:"text-red-500",children:"*"})]})}),e(f,{className:"pt-6",children:a("div",t?{className:"flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3",children:[a("div",{children:[e("p",{className:"text-sm font-medium text-green-800",children:t.name}),a("p",{className:"text-xs text-green-600",children:[t.totalQuestions," câu hỏi • ",t.parts.length," phần"]})]}),a("div",{className:"flex gap-2",children:[e(p,{type:"button",variant:"outline",size:"sm",onClick:n,children:"Đổi template"}),e(p,{type:"button",variant:"ghost",size:"icon",onClick:i,children:e(h,{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:"Chưa chọn template đề thi"}),a(p,{type:"button",variant:"outline",onClick:n,children:[e(c,{className:"mr-2 h-4 w-4"}),"Chọn template"]})]})})]})}function z(){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,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}from"react/jsx-runtime";import{useMemo as r,useRef as o,useState as i}from"react";import{ChevronLeft as a,ChevronRight as s,Save as n}from"lucide-react";import{Button as l}from"../../components/ui/button";import{QuestionCreator as d}from"../questions/creator/QuestionCreator";import{useToast as u}from"../../shared/lib/hooks/useToast";import{useT as m}from"../../shared/lib/i18n";import{ExamPartTabs as c}from"./ExamPartTabs";import{ExamQuestionGrid as p}from"./ExamQuestionGrid";import{PartAudioUploader as f}from"./PartAudioUploader";import{getEffectiveQuestionConfig as g,getPartByQuestionIndex as x,normalizeTemplateGroupConfig as h}from"./exam-template.utils";export function ExamCreator({template:b,examId:v,examDifficulty:w,examLevel:C,subjectId:y,initialQuestions:I=[],onSaveQuestion:N,onSave:S,onCancel:Q,onPartAudioChange:k,partAudioOverrides:q}){const A=m(),{success:E,warning:T,error:j}=u(),O=o(null),[D,M]=i(0),[P,L]=i(()=>Object.fromEntries(I.map(e=>[(e.questionNumber??1)-1,e]))),[_,U]=i(()=>new Set(I.map(e=>(e.questionNumber??1)-1))),[V,z]=i({}),[B,G]=i(!1),[R,$]=i(!1),H=o({}),F=x(b,D),J=F?h(F):void 0,K=F?g(F,D):void 0,W=!(!J?.groups||K?.groups),X=K&&"sectionName"in K?K.sectionName:void 0,Y=X&&!W?`${F?.id}_${X}`:F?.id,Z=P[D],ee=r(()=>({examId:v,templateId:b.id,questions:Object.values(P),currentQuestionIndex:D,lastSaved:(new Date).toISOString(),createdAt:(new Date).toISOString(),updatedAt:(new Date).toISOString(),isCompleted:_.size>=b.totalQuestions}),[_.size,D,v,P,b.id,b.totalQuestions]),te=e=>{R&&T({message:A("common.examCreator.saveBeforeExit")}),M(Math.max(0,Math.min(b.totalQuestions-1,e))),$(!1),H.current={}},re=async e=>{if(!F)return;if(!1===await(O.current?.triggerValidation?.()))return;const t={...Z,...H.current,...e,type:e?.type??H.current.type??Z?.type??K?.questionType,partId:F.id,questionNumber:D+1,difficulty:e?.difficulty??w??Z?.difficulty,skill:F.skill,isCompleted:!0};G(!0);try{const e=await(N?.({question:t,questionIndex:D,template:b,examId:v,examDifficulty:w,examLevel:C,subjectId:y}))||t;L(t=>({...t,[D]:e})),U(e=>new Set(e).add(D)),$(!1),E({message:A("common.examCreator.saveSuccess",{index:String(D+1)})})}catch(e){j({message:A("common.examCreator.saveError",{index:String(D+1)}),description:e instanceof Error?e.message:void 0})}finally{G(!1)}};return e("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(c,{template:b,currentPartId:F?.id??"",completedQuestions:_,onPartChange:e=>te(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"===F?.skill&&e(f,{partId:F.id,partName:F.name,audioUrl:q?.[F.id]??F.audioUrl??"",examId:v,onAudioChange:e=>{k?.(F.id,e)},isReadOnly:!k}),e(d,{initialData:Z,prefilledType:K?.questionType,prefilledSkill:F?.skill,optionCount:K?.optionCount,optionMode:K?.optionMode,defaultValue:K?.defaultValue,defaultQuestion:K?.defaultQuestion,viewMode:K?.viewMode,groups:K?.groups||J?.groups,groupConfig:K?.groupConfig||J?.groupConfig,groupType:K?.groupType||J?.groupType,articleCount:K?.articleCount??F?.articleCount,questionConfig:K?.questionConfig||F?.questionConfig,questionIndexInPart:W?D-(F?.startIndex??0):D-(K?.startIndex??F?.startIndex??0),partId:Y,onChange:e=>{H.current=e,$(!0)},onSave:re,onCancel:Q,isEmbedded:!0,externalErrors:V[D],validationRef:O,suggestedAnswers:K?.suggestedAnswers||F?.suggestedAnswers,hasSuggestedAnswers:K?.hasSuggestedAnswers??F?.hasSuggestedAnswers},D)]})}),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(l,{variant:"outline",onClick:()=>te(D-1),disabled:0===D,children:[e(a,{className:"mr-2 h-4 w-4"}),"Câu trước"]}),t(l,{onClick:()=>{re()},disabled:B,className:"bg-green-600 hover:bg-green-700",children:[e(n,{className:"mr-2 h-4 w-4"}),B?"Đang lưu...":"Lưu câu hỏi"]}),t(l,{variant:"outline",onClick:()=>te(D+1),disabled:D>=b.totalQuestions-1,children:["Câu tiếp",e(s,{className:"ml-2 h-4 w-4"})]})]})})]}),t("div",{className:"w-80 overflow-y-auto border-l border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:[e(p,{template:b,currentQuestionIndex:D,completedQuestions:_,onQuestionChange:te}),t(l,{onClick:()=>{(async()=>{R?T({message:A("common.examCreator.saveBeforeExit")}):await(S?.(ee))})()},className:"mt-4 w-full bg-blue-600 hover:bg-blue-700",children:[e(n,{className:"mr-2 h-4 w-4"}),"Hoàn tất đề thi"]})]})]})})}
@@ -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 s,Circle as r,Layers as a,Lock as l,Play as n}from"lucide-react";import{cn as d}from"../../shared/lib/utils";export function ExamPartTabs({template:s,currentPartId:r,completedQuestions:l,onPartChange:n}){const i=e=>{const t=e.endIndex-e.startIndex+1;let s=0;for(let t=e.startIndex;t<=e.endIndex;t++)l.has(t)&&s++;return{completed:s,total:t,percentage:t>0?Math.round(s/t*100):0}},o=e=>{const t=i(e);return 0===t.completed?"not-started":t.completed===t.total?"completed":"in-progress"};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(a,{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:[s.parts.length," phần"]})]})]}),e("div",{className:"space-y-3",children:s.parts.map((a,l)=>{const m=i(a),h=o(a),p=a.id===r,u=(x=l,s.parts.slice(0,x).every(e=>"completed"===o(e)));var x;return e("button",{onClick:()=>u&&n(a.id),disabled:!u,className:d("w-full rounded-xl border-2 p-4 text-left transition-all",p&&u?"border-transparent bg-blue-600 text-white shadow-lg":"border-gray-200 bg-white hover:border-gray-300",!u&&"cursor-not-allowed opacity-60"),children:t("div",{className:"flex items-start gap-3",children:[e(c,{status:h,active:p,unlocked:u}),t("div",{className:"min-w-0 flex-1",children:[t("div",{className:"flex items-center justify-between gap-2",children:[e("span",{className:"truncate text-sm font-semibold",children:a.name}),t("span",{className:"rounded-full bg-black/10 px-2 py-0.5 text-xs font-bold",children:[m.completed,"/",m.total]})]}),a.description&&e("p",{className:d("mt-1 text-xs",p?"text-blue-100":"text-gray-500"),children:a.description}),e("div",{className:d("mt-2 h-1.5 overflow-hidden rounded-full",p?"bg-white/20":"bg-gray-200"),children:e("div",{className:d("h-full rounded-full",p?"bg-white":"bg-green-500"),style:{width:`${m.percentage}%`}})})]})]})},a.id)})})]})}function c({status:t,active:a,unlocked:c}){const i=d("h-4 w-4",a?"text-white":"text-gray-500");return e(c?"completed"===t?s:"in-progress"===t?n:r:l,{className:i})}
@@ -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 l,getSectionForQuestionIndex as o}from"./exam-template.utils";export function ExamQuestionGrid({template:i,currentQuestionIndex:h,completedQuestions:m,onQuestionChange:u}){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 justify-center gap-3 overflow-y-auto px-3 py-2",children:Array.from({length:i.totalQuestions},(t,r)=>{const a=l(i,r),s=a?o(a,r):void 0;return e("button",{onClick:()=>u(r),className:c(r,h,m),title:[`Câu ${r+1}`,a?.name,s?.name].filter(Boolean).join(" - "),children:r+1},r)})})}),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"}),e(d,{icon:e(n,{className:"h-3.5 w-3.5 text-white"}),label:"Câu đang làm",className:"bg-blue-600"}),e(d,{icon:e(r,{className:"h-3.5 w-3.5 text-white"}),label:"Đã hoàn thành",className:"bg-green-500"}),e(d,{icon:e(a,{className:"h-3.5 w-3.5 text-gray-400"}),label:"Chưa tạo",className:"border bg-white"})]})]})}function c(e,t,r){return i("h-8 w-8 rounded-lg border-2 text-xs font-semibold shadow-sm transition-all hover:scale-105",e===t&&"border-transparent bg-blue-600 text-white ring-2 ring-blue-400",e!==t&&r.has(e)&&"border-transparent bg-green-500 text-white",e!==t&&!r.has(e)&&"border-gray-200 bg-white text-gray-500 hover:bg-gray-50")}function d({icon:r,label:a,className:s}){return t("div",{className:"mb-2 flex items-center gap-3",children:[e("div",{className:i("flex h-7 w-7 items-center justify-center rounded-lg",s),children:r}),e("span",{className:"text-xs font-medium text-gray-700",children:a})]})}
@@ -1,90 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Suspense, useEffect, useMemo, useState } from 'react';
4
- import { ArrowLeft, Hash, Loader2, Save } from 'lucide-react';
5
- import { Badge } from '../../components/ui/badge';
6
- import { Button } from '../../components/ui/button';
7
- import { useToast } from '../../shared/lib/hooks/useToast';
8
- import { useT } from '../../shared/lib/i18n';
9
- import { ExamCreator } from './ExamCreator';
10
- function ExamQuestionsContent({ api, examId, listPath = '/manage/exams', onNavigate }) {
11
- const t = useT();
12
- const { error: errorToast, success } = useToast();
13
- const [exam, setExam] = useState(null);
14
- const [questions, setQuestions] = useState([]);
15
- const [partAudioOverrides, setPartAudioOverrides] = useState({});
16
- const [isLoading, setIsLoading] = useState(true);
17
- const navigate = (href) => {
18
- if (onNavigate)
19
- onNavigate(href);
20
- else if (typeof window !== 'undefined')
21
- window.location.href = href;
22
- };
23
- useEffect(() => {
24
- let ignore = false;
25
- setIsLoading(true);
26
- Promise.all([api.getExam(examId), api.getQuestions?.(examId)])
27
- .then(([examResult, questionResult]) => {
28
- if (ignore)
29
- return;
30
- setExam(unwrap(examResult));
31
- setQuestions(unwrapOptional(questionResult) ?? []);
32
- })
33
- .catch((err) => errorToast({ message: t('admin.exams.questions.error'), description: err instanceof Error ? err.message : t('admin.exams.questions.loadError') }))
34
- .finally(() => !ignore && setIsLoading(false));
35
- return () => { ignore = true; };
36
- }, [api, examId, errorToast, t]);
37
- const template = useMemo(() => exam?.template ? normalizeTemplate(exam.template) : null, [exam?.template]);
38
- const handleSaveExam = async (examData) => {
39
- if (!template)
40
- return;
41
- await api.saveExam?.({ examId, questions: examData.questions, template, examData });
42
- navigate(listPath);
43
- };
44
- const handlePartAudioChange = async (partId, audioUrl) => {
45
- if (!template)
46
- return;
47
- const previous = partAudioOverrides[partId];
48
- setPartAudioOverrides((prev) => ({ ...prev, [partId]: audioUrl }));
49
- try {
50
- await api.updatePartAudio?.({ examId, partId, audioUrl, template });
51
- success({ message: t('admin.exams.partEditor.audioFile') });
52
- }
53
- catch (err) {
54
- setPartAudioOverrides((prev) => {
55
- const next = { ...prev };
56
- if (previous === undefined)
57
- delete next[partId];
58
- else
59
- next[partId] = previous;
60
- return next;
61
- });
62
- errorToast({ message: t('admin.exams.questions.error'), description: err instanceof Error ? err.message : undefined });
63
- }
64
- };
65
- if (isLoading)
66
- return _jsx(Loading, { label: t('admin.exams.questions.loading') });
67
- if (!exam || !template)
68
- return _jsx(Empty, { onBack: () => navigate(listPath), label: t('admin.exams.questions.noTemplate') });
69
- return (_jsxs("div", { className: "-m-4 flex h-[calc(100vh-4rem)] flex-col overflow-hidden md:-m-6 md:-mb-10", children: [_jsxs("div", { className: "flex flex-shrink-0 items-center gap-4 border-b border-gray-200/60 bg-white px-4 py-3", children: [_jsx(Button, { variant: "ghost", size: "icon", onClick: () => navigate(listPath), className: "h-8 w-8", children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsx("div", { className: "min-w-0 flex-1", children: _jsxs("h1", { className: "truncate text-lg font-semibold tracking-tight text-gray-900", children: [exam.name, exam.code && _jsxs(Badge, { variant: "outline", className: "ml-2", children: [_jsx(Hash, { className: "mr-0.5 h-3 w-3" }), exam.code] })] }) }), _jsxs(Button, { onClick: () => navigate(listPath), className: "gap-2 bg-blue-600 hover:bg-blue-700", size: "sm", children: [_jsx(Save, { className: "h-4 w-4" }), "Tho\u00E1t"] })] }), _jsx(ExamCreator, { template: template, examId: examId, examDifficulty: exam.difficulty, examLevel: exam.examLevel ?? undefined, subjectId: exam.subject?.id, initialQuestions: questions, onSaveQuestion: api.saveQuestion, onSave: handleSaveExam, onCancel: () => navigate(listPath), onPartAudioChange: api.updatePartAudio ? handlePartAudioChange : undefined, partAudioOverrides: partAudioOverrides })] }));
70
- }
71
- export function ExamQuestionsPageContainer(props) {
72
- return _jsx(Suspense, { fallback: _jsx(Loading, { label: "Loading..." }), children: _jsx(ExamQuestionsContent, { ...props }) });
73
- }
74
- function Loading({ label }) {
75
- return _jsx("div", { className: "flex h-screen items-center justify-center", children: _jsxs("div", { className: "flex flex-col items-center gap-3", children: [_jsx(Loader2, { className: "h-8 w-8 animate-spin text-blue-600" }), _jsx("p", { className: "text-sm text-gray-600", children: label })] }) });
76
- }
77
- function Empty({ label, onBack }) {
78
- return _jsx("div", { className: "flex h-screen items-center justify-center", children: _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-lg text-gray-900", children: label }), _jsx(Button, { onClick: onBack, className: "mt-4", children: "Quay l\u1EA1i" })] }) });
79
- }
80
- function unwrap(value) {
81
- return typeof value === 'object' && value !== null && 'data' in value ? value.data : value;
82
- }
83
- function unwrapOptional(value) {
84
- return value === undefined ? undefined : unwrap(value);
85
- }
86
- function normalizeTemplate(template) {
87
- if ('name' in template)
88
- return template;
89
- return { ...template, name: template.title, description: template.description || template.subTitle || '', level: template.examLevel, parts: template.parts.map((part) => ({ ...part, name: part.name || part.title || '' })) };
90
- }
1
+ "use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import{Suspense as t,useEffect as i,useMemo as n,useState as s}from"react";import{ArrowLeft as r,Hash as l,Loader2 as o,Save as m}from"lucide-react";import{Badge as c}from"../../components/ui/badge";import{Button as d}from"../../components/ui/button";import{useToast as u}from"../../shared/lib/hooks/useToast";import{useT as x}from"../../shared/lib/i18n";import{ExamCreator as h}from"./ExamCreator";function p({api:t,examId:o,listPath:p="/manage/exams",onNavigate:b}){const N=x(),{error:y,success:w}=u(),[k,j]=s(null),[E,q]=s([]),[C,I]=s({}),[P,Q]=s(!0),A=e=>{b?b(e):"undefined"!=typeof window&&(window.location.href=e)};i(()=>{let e=!1;return Q(!0),Promise.all([t.getExam(o),t.getQuestions?.(o)]).then(([a,t])=>{var i;e||(j(v(a)),q((void 0===(i=t)?void 0:v(i))??[]))}).catch(e=>y({message:N("admin.exams.questions.error"),description:e instanceof Error?e.message:N("admin.exams.questions.loadError")})).finally(()=>!e&&Q(!1)),()=>{e=!0}},[t,o,y,N]);const L=n(()=>k?.template?function(e){return"name"in e?e:{...e,name:e.title,description:e.description||e.subTitle||"",level:e.examLevel,parts:e.parts.map(e=>({...e,name:e.name||e.title||""}))}}(k.template):null,[k?.template]);return P?e(f,{label:N("admin.exams.questions.loading")}):k&&L?a("div",{className:"-m-4 flex h-[calc(100vh-4rem)] flex-col overflow-hidden md:-m-6 md:-mb-10",children:[a("div",{className:"flex flex-shrink-0 items-center gap-4 border-b border-gray-200/60 bg-white px-4 py-3",children:[e(d,{variant:"ghost",size:"icon",onClick:()=>A(p),className:"h-8 w-8",children:e(r,{className:"h-4 w-4"})}),e("div",{className:"min-w-0 flex-1",children:a("h1",{className:"truncate text-lg font-semibold tracking-tight text-gray-900",children:[k.name,k.code&&a(c,{variant:"outline",className:"ml-2",children:[e(l,{className:"mr-0.5 h-3 w-3"}),k.code]})]})}),a(d,{onClick:()=>A(p),className:"gap-2 bg-blue-600 hover:bg-blue-700",size:"sm",children:[e(m,{className:"h-4 w-4"}),"Thoát"]})]}),e(h,{template:L,examId:o,examDifficulty:k.difficulty,examLevel:k.examLevel??void 0,subjectId:k.subject?.id,initialQuestions:E,onSaveQuestion:t.saveQuestion,onSave:async e=>{L&&(await(t.saveExam?.({examId:o,questions:e.questions,template:L,examData:e})),A(p))},onCancel:()=>A(p),onPartAudioChange:t.updatePartAudio?async(e,a)=>{if(!L)return;const i=C[e];I(t=>({...t,[e]:a}));try{await(t.updatePartAudio?.({examId:o,partId:e,audioUrl:a,template:L})),w({message:N("admin.exams.partEditor.audioFile")})}catch(a){I(a=>{const t={...a};return void 0===i?delete t[e]:t[e]=i,t}),y({message:N("admin.exams.questions.error"),description:a instanceof Error?a.message:void 0})}}:void 0,partAudioOverrides:C})]}):e(g,{onBack:()=>A(p),label:N("admin.exams.questions.noTemplate")})}export function ExamQuestionsPageContainer(a){return e(t,{fallback:e(f,{label:"Loading..."}),children:e(p,{...a})})}function f({label:t}){return e("div",{className:"flex h-screen items-center justify-center",children:a("div",{className:"flex flex-col items-center gap-3",children:[e(o,{className:"h-8 w-8 animate-spin text-blue-600"}),e("p",{className:"text-sm text-gray-600",children:t})]})})}function g({label:t,onBack:i}){return e("div",{className:"flex h-screen items-center justify-center",children:a("div",{className:"text-center",children:[e("p",{className:"text-lg text-gray-900",children:t}),e(d,{onClick:i,className:"mt-4",children:"Quay lại"})]})})}function v(e){return"object"==typeof e&&null!==e&&"data"in e?e.data:e}
@@ -1,9 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Headphones, Music } from 'lucide-react';
4
- import { Card, CardContent } from '../../components/ui/card';
5
- import { FileUpload } from '../../components/ui/file-upload';
6
- import { Label } from '../../components/ui/label';
7
- export function PartAudioUploader({ partId, partName, audioUrl, onAudioChange, examId, isReadOnly = false }) {
8
- return (_jsxs(Card, { className: "mb-4 overflow-hidden border-0 bg-white shadow-lg shadow-sky-100/50", children: [_jsx("div", { className: "h-1.5 bg-gradient-to-r from-sky-500 via-cyan-500 to-teal-500" }), _jsx(CardContent, { className: "px-4 py-4", children: _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-sky-500 to-cyan-600 shadow-md", children: _jsx(Headphones, { className: "h-5 w-5 text-white" }) }), _jsxs("div", { children: [_jsxs(Label, { className: "text-sm font-bold text-gray-900", children: ["File \u00E2m thanh cho ", partName] }), _jsx("p", { className: "text-xs text-gray-500", children: "Audio d\u00F9ng chung cho t\u1EA5t c\u1EA3 c\u00E2u h\u1ECFi trong ph\u1EA7n n\u00E0y." })] })] }), _jsx("div", { className: "flex-1", children: _jsx(FileUpload, { id: `part-audio-${partId}`, label: "", accept: "audio/*", value: audioUrl || '', onChange: (url) => !isReadOnly && onAudioChange(url), placeholder: "Ch\u1ECDn file audio cho part n\u00E0y...", icon: _jsx(Music, { className: "h-4 w-4" }), maxSize: 100, autoUpload: true, examId: examId, partId: partId, showPlayButton: true, disabled: isReadOnly }) })] }) })] }));
9
- }
1
+ "use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import{Headphones as t,Music as o}from"lucide-react";import{Card as i,CardContent as r}from"../../components/ui/card";import{FileUpload as l}from"../../components/ui/file-upload";import{Label as s}from"../../components/ui/label";export function PartAudioUploader({partId:d,partName:n,audioUrl:c,onAudioChange:m,examId:h,isReadOnly:p=!1}){return a(i,{className:"mb-4 overflow-hidden border-0 bg-white shadow-lg shadow-sky-100/50",children:[e("div",{className:"h-1.5 bg-gradient-to-r from-sky-500 via-cyan-500 to-teal-500"}),e(r,{className:"px-4 py-4",children:a("div",{className:"flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4",children:[a("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-sky-500 to-cyan-600 shadow-md",children:e(t,{className:"h-5 w-5 text-white"})}),a("div",{children:[a(s,{className:"text-sm font-bold text-gray-900",children:["File âm thanh cho ",n]}),e("p",{className:"text-xs text-gray-500",children:"Audio dùng chung cho tất cả câu hỏi trong phần này."})]})]}),e("div",{className:"flex-1",children:e(l,{id:`part-audio-${d}`,label:"",accept:"audio/*",value:c||"",onChange:e=>!p&&m(e),placeholder:"Chọn file audio cho part này...",icon:e(o,{className:"h-4 w-4"}),maxSize:100,autoUpload:!0,examId:h,partId:d,showPlayButton:!0,disabled:p})})]})})]})}
@@ -1,66 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect, useState } from 'react';
4
- import { ChevronLeft, ChevronRight, Loader2, Search } from 'lucide-react';
5
- import { Button } from '../../components/ui/button';
6
- import { Card, CardContent } from '../../components/ui/card';
7
- import { Input } from '../../components/ui/input';
8
- import { useT } from '../../shared/lib/i18n';
9
- import { mapTemplateDetailToExamTemplate } from '../../api/exam-create';
10
- export function TemplateSelector({ api, onTemplateSelect, onCancel }) {
11
- const t = useT();
12
- const [search, setSearch] = useState('');
13
- const [page, setPage] = useState(1);
14
- const [templates, setTemplates] = useState([]);
15
- const [totalPages, setTotalPages] = useState(1);
16
- const [totalItems, setTotalItems] = useState(0);
17
- const [isLoading, setIsLoading] = useState(true);
18
- const [error, setError] = useState();
19
- const [loadingId, setLoadingId] = useState();
20
- // Reset to the first page whenever the search term changes so results stay in sync.
21
- useEffect(() => { setPage(1); }, [search]);
22
- useEffect(() => {
23
- let ignore = false;
24
- setIsLoading(true);
25
- api.getExamTemplates({ page, search: search || undefined })
26
- .then((result) => {
27
- if (ignore)
28
- return;
29
- setTemplates(result.items);
30
- setTotalPages(result.totalPages);
31
- setTotalItems(result.totalItems);
32
- })
33
- .catch((err) => { if (!ignore)
34
- setError(err instanceof Error ? err.message : t('admin.exams.questions.loadError')); })
35
- .finally(() => { if (!ignore)
36
- setIsLoading(false); });
37
- return () => { ignore = true; };
38
- }, [api, page, search, t]);
39
- // The list endpoint returns lightweight summaries (no `parts`). Fetch the full template detail by
40
- // id and map it to an `ExamTemplate` before emitting, so downstream (TemplateSection /
41
- // buildExamTemplatePayload) gets a complete entity. Ported from oe-exam-fe TemplateSelector.
42
- const handleSelect = async (summary) => {
43
- setError(undefined);
44
- setLoadingId(summary.id);
45
- try {
46
- const detail = unwrapDetail(await api.getExamTemplateDetail(summary.id));
47
- onTemplateSelect(mapTemplateDetailToExamTemplate(detail));
48
- }
49
- catch (err) {
50
- setError(err instanceof Error ? err.message : t('admin.exams.questions.loadError'));
51
- }
52
- finally {
53
- setLoadingId(undefined);
54
- }
55
- };
56
- const canPrev = page > 1 && !isLoading;
57
- const canNext = page < totalPages && !isLoading;
58
- return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-4", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" }), _jsx(Input, { value: search, onChange: (event) => setSearch(event.target.value), placeholder: t('admin.templates.searchPlaceholder'), className: "pl-9" })] }), isLoading ? _jsx("div", { className: "flex flex-1 items-center justify-center", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-blue-600" }) }) : null, !isLoading && error ? _jsx("p", { className: "text-sm text-red-600", children: error }) : null, !isLoading && !error ? _jsx("div", { className: "grid flex-1 content-start gap-3 overflow-y-auto md:grid-cols-2 lg:grid-cols-3", children: templates.map((template) => _jsx(TemplateCard, { template: template, onSelect: handleSelect, isLoading: loadingId === template.id, disabled: !!loadingId }, template.id)) }) : null, !isLoading && !error && templates.length === 0 ? _jsx("p", { className: "flex-1 text-center text-sm text-gray-500", children: t('admin.templates.table.empty') }) : null, !isLoading && !error && totalItems > 0 ? (_jsxs("div", { className: "flex shrink-0 items-center justify-between border-t border-gray-100 pt-3", children: [_jsx("span", { className: "text-sm text-gray-500", children: t('admin.templates.table.showing', { count: templates.length, total: totalItems }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", disabled: !canPrev, onClick: () => setPage((value) => Math.max(1, value - 1)), children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), t('common.pagination.previous')] }), _jsx("span", { className: "text-sm text-gray-600", children: t('admin.schedules.list.page', { current: page, total: totalPages }) }), _jsxs(Button, { variant: "outline", size: "sm", disabled: !canNext, onClick: () => setPage((value) => value + 1), children: [t('common.pagination.next'), _jsx(ChevronRight, { className: "h-4 w-4" })] })] })] })) : null, onCancel && _jsx("div", { className: "flex shrink-0 justify-end", children: _jsx(Button, { variant: "outline", onClick: onCancel, children: t('common.cancel') }) })] }));
59
- }
60
- function TemplateCard({ template, onSelect, isLoading, disabled }) {
61
- return (_jsx(Card, { className: `relative cursor-pointer transition hover:border-blue-300 hover:shadow-sm ${disabled ? 'pointer-events-none opacity-60' : ''}`, onClick: () => onSelect(template), children: _jsxs(CardContent, { className: "space-y-2 p-4", children: [_jsx("h3", { className: "line-clamp-2 font-medium text-gray-900", children: template.title }), _jsx("p", { className: "line-clamp-2 text-sm text-gray-500", children: template.subTitle }), _jsxs("p", { className: "text-xs text-gray-500", children: [template.totalQuestions, " c\u00E2u h\u1ECFi \u2022 ", template.totalParts, " ph\u1EA7n"] }), isLoading ? _jsx("div", { className: "absolute inset-0 flex items-center justify-center rounded-xl bg-white/60", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-blue-600" }) }) : null] }) }));
62
- }
63
- /** Unwrap the optional `{ data }` envelope returned by the detail endpoint. */
64
- function unwrapDetail(result) {
65
- return 'data' in result ? result.data : result;
66
- }
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useEffect as a,useState as l}from"react";import{ChevronLeft as s,ChevronRight as i,Loader2 as n,Search as r}from"lucide-react";import{Button as c}from"../../components/ui/button";import{Card as o,CardContent as m}from"../../components/ui/card";import{Input as d}from"../../components/ui/input";import{useT as p}from"../../shared/lib/i18n";import{mapTemplateDetailToExamTemplate as h}from"../../api/exam-create";export function TemplateSelector({api:o,onTemplateSelect:m,onCancel:x}){const f=p(),[g,v]=l(""),[N,y]=l(1),[b,w]=l([]),[j,k]=l(1),[C,E]=l(0),[T,S]=l(!0),[P,q]=l(),[z,L]=l();a(()=>{y(1)},[g]),a(()=>{let e=!1;return S(!0),o.getExamTemplates({page:N,search:g||void 0}).then(t=>{e||(w(t.items),k(t.totalPages),E(t.totalItems))}).catch(t=>{e||q(t instanceof Error?t.message:f("admin.exams.questions.loadError"))}).finally(()=>{e||S(!1)}),()=>{e=!0}},[o,N,g,f]);const D=async e=>{q(void 0),L(e.id);try{const a="data"in(t=await o.getExamTemplateDetail(e.id))?t.data:t;m(h(a))}catch(e){q(e instanceof Error?e.message:f("admin.exams.questions.loadError"))}finally{L(void 0)}var t},I=N>1&&!T,M=N<j&&!T;return t("div",{className:"flex min-h-0 flex-1 flex-col gap-4",children:[t("div",{className:"relative",children:[e(r,{className:"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"}),e(d,{value:g,onChange:e=>v(e.target.value),placeholder:f("admin.templates.searchPlaceholder"),className:"pl-9"})]}),T?e("div",{className:"flex flex-1 items-center justify-center",children:e(n,{className:"h-6 w-6 animate-spin text-blue-600"})}):null,!T&&P?e("p",{className:"text-sm text-red-600",children:P}):null,T||P?null:e("div",{className:"grid flex-1 content-start gap-3 overflow-y-auto md:grid-cols-2 lg:grid-cols-3",children:b.map(t=>e(u,{template:t,onSelect:D,isLoading:z===t.id,disabled:!!z},t.id))}),T||P||0!==b.length?null:e("p",{className:"flex-1 text-center text-sm text-gray-500",children:f("admin.templates.table.empty")}),!T&&!P&&C>0?t("div",{className:"flex shrink-0 items-center justify-between border-t border-gray-100 pt-3",children:[e("span",{className:"text-sm text-gray-500",children:f("admin.templates.table.showing",{count:b.length,total:C})}),t("div",{className:"flex items-center gap-2",children:[t(c,{variant:"outline",size:"sm",disabled:!I,onClick:()=>y(e=>Math.max(1,e-1)),children:[e(s,{className:"h-4 w-4"}),f("common.pagination.previous")]}),e("span",{className:"text-sm text-gray-600",children:f("admin.schedules.list.page",{current:N,total:j})}),t(c,{variant:"outline",size:"sm",disabled:!M,onClick:()=>y(e=>e+1),children:[f("common.pagination.next"),e(i,{className:"h-4 w-4"})]})]})]}):null,x&&e("div",{className:"flex shrink-0 justify-end",children:e(c,{variant:"outline",onClick:x,children:f("common.cancel")})})]})}function u({template:a,onSelect:l,isLoading:s,disabled:i}){return e(o,{className:"relative cursor-pointer transition hover:border-blue-300 hover:shadow-sm "+(i?"pointer-events-none opacity-60":""),onClick:()=>l(a),children:t(m,{className:"space-y-2 p-4",children:[e("h3",{className:"line-clamp-2 font-medium text-gray-900",children:a.title}),e("p",{className:"line-clamp-2 text-sm text-gray-500",children:a.subTitle}),t("p",{className:"text-xs text-gray-500",children:[a.totalQuestions," câu hỏi • ",a.totalParts," phần"]}),s?e("div",{className:"absolute inset-0 flex items-center justify-center rounded-xl bg-white/60",children:e(n,{className:"h-5 w-5 animate-spin text-blue-600"})}):null]})})}
@@ -1,3 +1 @@
1
- export { buildExamTemplatePayload, createExamCreateApi, createMockCreateExamApi } from '../../api/exam-create';
2
- export { createExamQuestionsApi, createExamQuestionsFetchApi, createMockExamQuestionsApi } from '../../api/exam-questions';
3
- export { createExamTakingApi, createMockExamTakingApi } from '../../api/exam-taking';
1
+ export{buildExamTemplatePayload,createExamCreateApi,createMockCreateExamApi}from"../../api/exam-create";export{createExamQuestionsApi,createExamQuestionsFetchApi,createMockExamQuestionsApi}from"../../api/exam-questions";export{createExamTakingApi,createMockExamTakingApi}from"../../api/exam-taking";
@@ -1,18 +1 @@
1
- import { z } from 'zod';
2
- export const examFormSchema = z.object({
3
- code: z.string().min(1, 'Mã đề thi không được để trống').max(100, 'Mã đề thi không được vượt quá 100 ký tự').regex(/^[a-zA-Z0-9_-]+$/, 'Mã đề thi chỉ được chứa chữ cái, số, dấu gạch ngang (-) và gạch dưới (_)').trim(),
4
- name: z.string().min(3, 'Tên đề thi phải có ít nhất 3 ký tự').max(255, 'Tên đề thi không được vượt quá 255 ký tự').trim(),
5
- level: z.string(),
6
- difficulty: z.string(),
7
- duration: z.string().refine((value) => {
8
- if (!value)
9
- return true;
10
- const minutes = Number.parseInt(value, 10);
11
- return !Number.isNaN(minutes) && minutes >= 5 && minutes <= 300;
12
- }, { message: 'Thời gian thi phải từ 5 đến 300 phút' }),
13
- totalPoints: z.string().min(1, 'Vui lòng nhập tổng điểm').refine((value) => /^[1-9]\d*$/.test(value.trim()), { message: 'Tổng điểm phải là số nguyên dương' }).refine((value) => {
14
- const points = Number.parseInt(value, 10);
15
- return points >= 1 && points <= 1000;
16
- }, { message: 'Tổng điểm phải từ 1 đến 1000' }),
17
- description: z.string().max(2000, 'Mô tả không được vượt quá 2000 ký tự').trim(),
18
- });
1
+ import{z as t}from"zod";export const examFormSchema=t.object({code:t.string().min(1,"Mã đề thi không được để trống").max(100,"Mã đề thi không được vượt quá 100 ký tự").regex(/^[a-zA-Z0-9_-]+$/,"Mã đề thi chỉ được chứa chữ cái, số, dấu gạch ngang (-) và gạch dưới (_)").trim(),name:t.string().min(3,"Tên đề thi phải có ít nhất 3 ký tự").max(255,"Tên đề thi không được vượt quá 255 ký tự").trim(),level:t.string(),difficulty:t.string(),duration:t.string().refine(t=>{if(!t)return!0;const n=Number.parseInt(t,10);return!Number.isNaN(n)&&n>=5&&n<=300},{message:"Thời gian thi phải từ 5 đến 300 phút"}),totalPoints:t.string().min(1,"Vui lòng nhập tổng điểm").refine(t=>/^[1-9]\d*$/.test(t.trim()),{message:"Tổng điểm phải là số nguyên dương"}).refine(t=>{const n=Number.parseInt(t,10);return n>=1&&n<=1e3},{message:"Tổng điểm phải từ 1 đến 1000"}),description:t.string().max(2e3,"Mô tả không được vượt quá 2000 ký tự").trim()});
@@ -1,18 +1 @@
1
- export function normalizeTemplateGroupConfig(config) {
2
- return {
3
- ...config,
4
- groups: config.groups ?? config.group?.items,
5
- groupConfig: config.groupConfig ?? config.group?.config,
6
- groupType: config.groupType ?? config.group?.type,
7
- };
8
- }
9
- export function getPartByQuestionIndex(template, questionIndex) {
10
- return template.parts.find((part) => questionIndex >= part.startIndex && questionIndex <= part.endIndex);
11
- }
12
- export function getSectionForQuestionIndex(part, questionIndex) {
13
- return part.sections?.find((section) => questionIndex >= section.startIndex && questionIndex <= section.endIndex);
14
- }
15
- export function getEffectiveQuestionConfig(part, questionIndex) {
16
- const section = getSectionForQuestionIndex(part, questionIndex);
17
- return section ? { ...normalizeTemplateGroupConfig(section), sectionName: section.name } : normalizeTemplateGroupConfig(part);
18
- }
1
+ export function normalizeTemplateGroupConfig(e){return{...e,groups:e.groups??e.group?.items,groupConfig:e.groupConfig??e.group?.config,groupType:e.groupType??e.group?.type}}export function getPartByQuestionIndex(e,n){return e.parts.find(e=>n>=e.startIndex&&n<=e.endIndex)}export function getSectionForQuestionIndex(e,n){return e.sections?.find(e=>n>=e.startIndex&&n<=e.endIndex)}export function getEffectiveQuestionConfig(e,n){const o=getSectionForQuestionIndex(e,n);return o?{...normalizeTemplateGroupConfig(o),sectionName:o.name}:normalizeTemplateGroupConfig(e)}
@@ -1,10 +1 @@
1
- export * from './take';
2
- export { CreateExamPageContainer } from './CreateExamPageContainer';
3
- export { CreateExamInfoForm } from './CreateExamInfoForm';
4
- export { TemplateSelector } from './TemplateSelector';
5
- export { ExamCreator } from './ExamCreator';
6
- export { ExamPartTabs } from './ExamPartTabs';
7
- export { ExamQuestionGrid } from './ExamQuestionGrid';
8
- export { PartAudioUploader } from './PartAudioUploader';
9
- export { createExamQuestionsApi, createExamQuestionsFetchApi, createMockExamQuestionsApi } from './api';
10
- export { ExamQuestionsPageContainer } from './ExamQuestionsPageContainer';
1
+ export*from"./take";export{CreateExamPageContainer}from"./CreateExamPageContainer";export{CreateExamInfoForm}from"./CreateExamInfoForm";export{TemplateSelector}from"./TemplateSelector";export{ExamCreator}from"./ExamCreator";export{ExamPartTabs}from"./ExamPartTabs";export{ExamQuestionGrid}from"./ExamQuestionGrid";export{PartAudioUploader}from"./PartAudioUploader";export{createExamQuestionsApi,createExamQuestionsFetchApi,createMockExamQuestionsApi}from"./api";export{ExamQuestionsPageContainer}from"./ExamQuestionsPageContainer";