@hed-hog/lms 0.0.358 → 0.0.364

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 (330) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +65 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +72 -0
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts +8 -0
  6. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts.map +1 -0
  7. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js +40 -0
  8. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js.map +1 -0
  9. package/dist/class-group/class-group.controller.d.ts +16 -16
  10. package/dist/class-group/class-group.service.d.ts +12 -12
  11. package/dist/course/course-audio-transcription.service.d.ts +3 -2
  12. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  13. package/dist/course/course-audio-transcription.service.js +49 -8
  14. package/dist/course/course-audio-transcription.service.js.map +1 -1
  15. package/dist/course/course-lesson.controller.d.ts +4 -0
  16. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  17. package/dist/course/course-lesson.controller.js +10 -0
  18. package/dist/course/course-lesson.controller.js.map +1 -1
  19. package/dist/course/course-structure.controller.d.ts +11 -4
  20. package/dist/course/course-structure.controller.d.ts.map +1 -1
  21. package/dist/course/course-structure.controller.js +14 -0
  22. package/dist/course/course-structure.controller.js.map +1 -1
  23. package/dist/course/course-structure.service.d.ts +15 -1
  24. package/dist/course/course-structure.service.d.ts.map +1 -1
  25. package/dist/course/course-structure.service.js +139 -4
  26. package/dist/course/course-structure.service.js.map +1 -1
  27. package/dist/course/course-video-conversion.service.d.ts +8 -0
  28. package/dist/course/course-video-conversion.service.d.ts.map +1 -1
  29. package/dist/course/course-video-conversion.service.js +87 -51
  30. package/dist/course/course-video-conversion.service.js.map +1 -1
  31. package/dist/course/course.controller.d.ts +73 -1
  32. package/dist/course/course.controller.d.ts.map +1 -1
  33. package/dist/course/course.controller.js +27 -3
  34. package/dist/course/course.controller.js.map +1 -1
  35. package/dist/course/course.module.d.ts.map +1 -1
  36. package/dist/course/course.module.js +15 -3
  37. package/dist/course/course.module.js.map +1 -1
  38. package/dist/course/course.service.d.ts +108 -4
  39. package/dist/course/course.service.d.ts.map +1 -1
  40. package/dist/course/course.service.js +631 -30
  41. package/dist/course/course.service.js.map +1 -1
  42. package/dist/course/dto/cleanup-course-storage.dto.d.ts +6 -0
  43. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -0
  44. package/dist/course/dto/cleanup-course-storage.dto.js +33 -0
  45. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -0
  46. package/dist/course/dto/cleanup-upload-history.dto.d.ts +9 -0
  47. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -0
  48. package/dist/course/dto/cleanup-upload-history.dto.js +36 -0
  49. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -0
  50. package/dist/course/dto/create-course-bulk-job.dto.d.ts +4 -0
  51. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -0
  52. package/dist/course/dto/create-course-bulk-job.dto.js +21 -0
  53. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -0
  54. package/dist/course/lms-bulk-upload-automation.service.d.ts +39 -0
  55. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -0
  56. package/dist/course/lms-bulk-upload-automation.service.js +443 -0
  57. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -0
  58. package/dist/course/lms-bulk-upload-infra.service.d.ts +31 -0
  59. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -0
  60. package/dist/course/lms-bulk-upload-infra.service.js +277 -0
  61. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -0
  62. package/dist/course/lms-bulk-upload.constants.d.ts +4 -0
  63. package/dist/course/lms-bulk-upload.constants.d.ts.map +1 -0
  64. package/dist/course/lms-bulk-upload.constants.js +7 -0
  65. package/dist/course/lms-bulk-upload.constants.js.map +1 -0
  66. package/dist/course/lms-bulk-upload.controller.d.ts +153 -0
  67. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -0
  68. package/dist/course/lms-bulk-upload.controller.js +129 -0
  69. package/dist/course/lms-bulk-upload.controller.js.map +1 -0
  70. package/dist/course/lms-bulk-upload.service.d.ts +181 -0
  71. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -0
  72. package/dist/course/lms-bulk-upload.service.js +754 -0
  73. package/dist/course/lms-bulk-upload.service.js.map +1 -0
  74. package/dist/course/lms-setting.controller.d.ts +7 -1
  75. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  76. package/dist/course/lms-setting.controller.js +26 -3
  77. package/dist/course/lms-setting.controller.js.map +1 -1
  78. package/dist/enterprise/enterprise.controller.d.ts +20 -20
  79. package/dist/enterprise/enterprise.service.d.ts +20 -20
  80. package/dist/enterprise/training/training-admin.controller.d.ts +11 -11
  81. package/dist/enterprise/training/training-admin.service.d.ts +11 -11
  82. package/dist/enterprise/training/training-instructor.controller.d.ts +2 -2
  83. package/dist/enterprise/training/training-instructor.service.d.ts +2 -2
  84. package/dist/enterprise/training/training-student.controller.d.ts +1 -1
  85. package/dist/enterprise/training/training-student.service.d.ts +1 -1
  86. package/dist/enterprise/training/training-viewer.controller.d.ts +2 -2
  87. package/dist/evaluation/evaluation.controller.d.ts +8 -8
  88. package/dist/evaluation/evaluation.service.d.ts +8 -8
  89. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts +6 -0
  90. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts.map +1 -0
  91. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js +34 -0
  92. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js.map +1 -0
  93. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts +28 -0
  94. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts.map +1 -0
  95. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js +123 -0
  96. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js.map +1 -0
  97. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts +4 -0
  98. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts.map +1 -0
  99. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js +22 -0
  100. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js.map +1 -0
  101. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts +10 -0
  102. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts.map +1 -0
  103. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js +52 -0
  104. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js.map +1 -0
  105. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts +15 -0
  106. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts.map +1 -0
  107. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js +86 -0
  108. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js.map +1 -0
  109. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +26 -0
  110. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -0
  111. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +304 -0
  112. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -0
  113. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts +87 -0
  114. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts.map +1 -0
  115. package/dist/lesson-xp-map/lesson-xp-map.controller.js +185 -0
  116. package/dist/lesson-xp-map/lesson-xp-map.controller.js.map +1 -0
  117. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts +3 -0
  118. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -0
  119. package/dist/lesson-xp-map/lesson-xp-map.module.js +34 -0
  120. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -0
  121. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts +84 -0
  122. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts.map +1 -0
  123. package/dist/lesson-xp-map/lesson-xp-map.service.js +353 -0
  124. package/dist/lesson-xp-map/lesson-xp-map.service.js.map +1 -0
  125. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts +10 -0
  126. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts.map +1 -0
  127. package/dist/lesson-xp-map/lesson-xp-segment.controller.js +63 -0
  128. package/dist/lesson-xp-map/lesson-xp-segment.controller.js.map +1 -0
  129. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts +27 -0
  130. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts.map +1 -0
  131. package/dist/lesson-xp-map/lesson-xp-segment.service.js +194 -0
  132. package/dist/lesson-xp-map/lesson-xp-segment.service.js.map +1 -0
  133. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -0
  134. package/dist/lms.module.d.ts.map +1 -1
  135. package/dist/lms.module.js +17 -2
  136. package/dist/lms.module.js.map +1 -1
  137. package/dist/platforma/dto/update-profile.dto.d.ts +17 -0
  138. package/dist/platforma/dto/update-profile.dto.d.ts.map +1 -0
  139. package/dist/platforma/dto/update-profile.dto.js +87 -0
  140. package/dist/platforma/dto/update-profile.dto.js.map +1 -0
  141. package/dist/platforma/platforma.controller.d.ts +94 -7
  142. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  143. package/dist/platforma/platforma.controller.js +85 -2
  144. package/dist/platforma/platforma.controller.js.map +1 -1
  145. package/dist/platforma/platforma.service.d.ts +27 -0
  146. package/dist/platforma/platforma.service.d.ts.map +1 -0
  147. package/dist/platforma/platforma.service.js +274 -0
  148. package/dist/platforma/platforma.service.js.map +1 -0
  149. package/dist/student-xp/student-xp.controller.d.ts +41 -0
  150. package/dist/student-xp/student-xp.controller.d.ts.map +1 -0
  151. package/dist/student-xp/student-xp.controller.js +114 -0
  152. package/dist/student-xp/student-xp.controller.js.map +1 -0
  153. package/dist/student-xp/student-xp.module.d.ts +3 -0
  154. package/dist/student-xp/student-xp.module.d.ts.map +1 -0
  155. package/dist/student-xp/student-xp.module.js +25 -0
  156. package/dist/student-xp/student-xp.module.js.map +1 -0
  157. package/dist/student-xp/student-xp.service.d.ts +65 -0
  158. package/dist/student-xp/student-xp.service.d.ts.map +1 -0
  159. package/dist/student-xp/student-xp.service.js +197 -0
  160. package/dist/student-xp/student-xp.service.js.map +1 -0
  161. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts +12 -0
  162. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts.map +1 -0
  163. package/dist/xp-catalog/dto/create-xp-area.dto.js +63 -0
  164. package/dist/xp-catalog/dto/create-xp-area.dto.js.map +1 -0
  165. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts +11 -0
  166. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts.map +1 -0
  167. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js +57 -0
  168. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js.map +1 -0
  169. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts +11 -0
  170. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts.map +1 -0
  171. package/dist/xp-catalog/dto/create-xp-skill.dto.js +57 -0
  172. package/dist/xp-catalog/dto/create-xp-skill.dto.js.map +1 -0
  173. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts +12 -0
  174. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts.map +1 -0
  175. package/dist/xp-catalog/dto/update-xp-area.dto.js +66 -0
  176. package/dist/xp-catalog/dto/update-xp-area.dto.js.map +1 -0
  177. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts +11 -0
  178. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts.map +1 -0
  179. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js +60 -0
  180. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js.map +1 -0
  181. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts +11 -0
  182. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts.map +1 -0
  183. package/dist/xp-catalog/dto/update-xp-skill.dto.js +60 -0
  184. package/dist/xp-catalog/dto/update-xp-skill.dto.js.map +1 -0
  185. package/dist/xp-catalog/xp-area.controller.d.ts +25 -0
  186. package/dist/xp-catalog/xp-area.controller.d.ts.map +1 -0
  187. package/dist/xp-catalog/xp-area.controller.js +105 -0
  188. package/dist/xp-catalog/xp-area.controller.js.map +1 -0
  189. package/dist/xp-catalog/xp-area.service.d.ts +35 -0
  190. package/dist/xp-catalog/xp-area.service.d.ts.map +1 -0
  191. package/dist/xp-catalog/xp-area.service.js +168 -0
  192. package/dist/xp-catalog/xp-area.service.js.map +1 -0
  193. package/dist/xp-catalog/xp-catalog.module.d.ts +3 -0
  194. package/dist/xp-catalog/xp-catalog.module.d.ts.map +1 -0
  195. package/dist/xp-catalog/xp-catalog.module.js +29 -0
  196. package/dist/xp-catalog/xp-catalog.module.js.map +1 -0
  197. package/dist/xp-catalog/xp-learning-type.controller.d.ts +20 -0
  198. package/dist/xp-catalog/xp-learning-type.controller.d.ts.map +1 -0
  199. package/dist/xp-catalog/xp-learning-type.controller.js +96 -0
  200. package/dist/xp-catalog/xp-learning-type.controller.js.map +1 -0
  201. package/dist/xp-catalog/xp-learning-type.service.d.ts +30 -0
  202. package/dist/xp-catalog/xp-learning-type.service.d.ts.map +1 -0
  203. package/dist/xp-catalog/xp-learning-type.service.js +146 -0
  204. package/dist/xp-catalog/xp-learning-type.service.js.map +1 -0
  205. package/dist/xp-catalog/xp-skill.controller.d.ts +26 -0
  206. package/dist/xp-catalog/xp-skill.controller.d.ts.map +1 -0
  207. package/dist/xp-catalog/xp-skill.controller.js +113 -0
  208. package/dist/xp-catalog/xp-skill.controller.js.map +1 -0
  209. package/dist/xp-catalog/xp-skill.service.d.ts +37 -0
  210. package/dist/xp-catalog/xp-skill.service.d.ts.map +1 -0
  211. package/dist/xp-catalog/xp-skill.service.js +174 -0
  212. package/dist/xp-catalog/xp-skill.service.js.map +1 -0
  213. package/hedhog/data/menu.yaml +101 -0
  214. package/hedhog/data/role.yaml +8 -0
  215. package/hedhog/data/route.yaml +547 -0
  216. package/hedhog/data/setting_group.yaml +33 -0
  217. package/hedhog/data/xp_area.yaml +164 -0
  218. package/hedhog/data/xp_learning_type.yaml +131 -0
  219. package/hedhog/data/xp_skill.yaml +1834 -0
  220. package/hedhog/frontend/app/achievements/page.tsx.ejs +108 -118
  221. package/hedhog/frontend/app/bitcodes/page.tsx.ejs +22 -34
  222. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +1453 -0
  223. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +21 -45
  224. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +40 -74
  225. package/hedhog/frontend/app/classes/page.tsx.ejs +56 -85
  226. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +3 -2
  227. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +48 -5
  228. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +4 -4
  229. package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +19 -2
  230. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +1170 -0
  231. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +16 -0
  232. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +2 -0
  233. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +623 -0
  234. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +1458 -0
  235. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +55 -2
  236. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +442 -104
  237. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +296 -49
  238. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +3 -0
  239. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +1 -0
  240. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +101 -85
  241. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +21 -1
  242. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +3 -0
  243. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +7 -1
  244. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +44 -0
  245. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +54 -0
  246. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +52 -0
  247. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-xp-overview.ts.ejs +76 -0
  248. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-xp-map.ts.ejs +128 -0
  249. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +30 -0
  250. package/hedhog/frontend/app/courses/[id]/structure/_utils/xp-color-config.ts.ejs +115 -0
  251. package/hedhog/frontend/app/courses/_components/CourseDeleteDialog.tsx.ejs +223 -0
  252. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +89 -0
  253. package/hedhog/frontend/app/courses/page.tsx.ejs +400 -230
  254. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -63
  255. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +53 -77
  256. package/hedhog/frontend/app/exams/page.tsx.ejs +54 -90
  257. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +23 -36
  258. package/hedhog/frontend/app/instructors/page.tsx.ejs +72 -81
  259. package/hedhog/frontend/app/paths/page.tsx.ejs +40 -68
  260. package/hedhog/frontend/app/training/page.tsx.ejs +40 -68
  261. package/hedhog/frontend/app/xp/areas/page.tsx.ejs +782 -0
  262. package/hedhog/frontend/app/xp/learning-types/page.tsx.ejs +690 -0
  263. package/hedhog/frontend/app/xp/skills/page.tsx.ejs +811 -0
  264. package/hedhog/frontend/messages/en.json +386 -3
  265. package/hedhog/frontend/messages/pt.json +386 -3
  266. package/hedhog/table/lesson_xp_map.yaml +50 -0
  267. package/hedhog/table/lesson_xp_segment.yaml +40 -0
  268. package/hedhog/table/lesson_xp_segment_area.yaml +24 -0
  269. package/hedhog/table/lesson_xp_segment_learning_type.yaml +24 -0
  270. package/hedhog/table/lesson_xp_segment_skill.yaml +24 -0
  271. package/hedhog/table/lms_bulk_upload_item.yaml +44 -0
  272. package/hedhog/table/lms_bulk_upload_session.yaml +42 -0
  273. package/hedhog/table/student_area_xp.yaml +30 -0
  274. package/hedhog/table/student_learning_type_xp.yaml +30 -0
  275. package/hedhog/table/student_skill_xp.yaml +30 -0
  276. package/hedhog/table/student_xp_event.yaml +34 -0
  277. package/hedhog/table/xp_area.yaml +39 -0
  278. package/hedhog/table/xp_learning_type.yaml +34 -0
  279. package/hedhog/table/xp_skill.yaml +39 -0
  280. package/package.json +9 -8
  281. package/src/bitcode-wallet/bitcode-wallet.service.ts +113 -0
  282. package/src/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.ts +32 -0
  283. package/src/course/course-audio-transcription.service.ts +58 -21
  284. package/src/course/course-lesson.controller.ts +6 -1
  285. package/src/course/course-structure.controller.ts +10 -0
  286. package/src/course/course-structure.service.ts +174 -1
  287. package/src/course/course-video-conversion.service.ts +113 -75
  288. package/src/course/course.controller.ts +22 -3
  289. package/src/course/course.module.ts +15 -3
  290. package/src/course/course.service.ts +847 -30
  291. package/src/course/dto/cleanup-course-storage.dto.ts +22 -0
  292. package/src/course/dto/cleanup-upload-history.dto.ts +26 -0
  293. package/src/course/dto/create-course-bulk-job.dto.ts +6 -0
  294. package/src/course/lms-bulk-upload-automation.service.ts +560 -0
  295. package/src/course/lms-bulk-upload-infra.service.ts +327 -0
  296. package/src/course/lms-bulk-upload.constants.ts +5 -0
  297. package/src/course/lms-bulk-upload.controller.ts +103 -0
  298. package/src/course/lms-bulk-upload.service.ts +1029 -0
  299. package/src/course/lms-setting.controller.ts +28 -1
  300. package/src/lesson-xp-map/dto/create-lesson-xp-map.dto.ts +17 -0
  301. package/src/lesson-xp-map/dto/create-lesson-xp-segment.dto.ts +102 -0
  302. package/src/lesson-xp-map/dto/review-lesson-xp-map.dto.ts +7 -0
  303. package/src/lesson-xp-map/dto/update-lesson-xp-map.dto.ts +36 -0
  304. package/src/lesson-xp-map/dto/update-lesson-xp-segment.dto.ts +78 -0
  305. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +396 -0
  306. package/src/lesson-xp-map/lesson-xp-map.controller.ts +116 -0
  307. package/src/lesson-xp-map/lesson-xp-map.module.ts +21 -0
  308. package/src/lesson-xp-map/lesson-xp-map.service.ts +442 -0
  309. package/src/lesson-xp-map/lesson-xp-segment.controller.ts +36 -0
  310. package/src/lesson-xp-map/lesson-xp-segment.service.ts +229 -0
  311. package/src/lms.module.ts +17 -2
  312. package/src/platforma/dto/update-profile.dto.ts +59 -0
  313. package/src/platforma/platforma.controller.ts +57 -2
  314. package/src/platforma/platforma.service.ts +268 -0
  315. package/src/student-xp/student-xp.controller.ts +76 -0
  316. package/src/student-xp/student-xp.module.ts +12 -0
  317. package/src/student-xp/student-xp.service.ts +236 -0
  318. package/src/xp-catalog/dto/create-xp-area.dto.ts +40 -0
  319. package/src/xp-catalog/dto/create-xp-learning-type.dto.ts +35 -0
  320. package/src/xp-catalog/dto/create-xp-skill.dto.ts +35 -0
  321. package/src/xp-catalog/dto/update-xp-area.dto.ts +43 -0
  322. package/src/xp-catalog/dto/update-xp-learning-type.dto.ts +38 -0
  323. package/src/xp-catalog/dto/update-xp-skill.dto.ts +38 -0
  324. package/src/xp-catalog/xp-area.controller.ts +64 -0
  325. package/src/xp-catalog/xp-area.service.ts +196 -0
  326. package/src/xp-catalog/xp-catalog.module.ts +16 -0
  327. package/src/xp-catalog/xp-learning-type.controller.ts +59 -0
  328. package/src/xp-catalog/xp-learning-type.service.ts +170 -0
  329. package/src/xp-catalog/xp-skill.controller.ts +71 -0
  330. package/src/xp-catalog/xp-skill.service.ts +205 -0
@@ -0,0 +1,1170 @@
1
+ 'use client';
2
+
3
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
4
+ import { useQueryClient } from '@tanstack/react-query';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Badge } from '@/components/ui/badge';
7
+ import {
8
+ AlertDialog,
9
+ AlertDialogAction,
10
+ AlertDialogCancel,
11
+ AlertDialogContent,
12
+ AlertDialogDescription,
13
+ AlertDialogFooter,
14
+ AlertDialogHeader,
15
+ AlertDialogTitle,
16
+ } from '@/components/ui/alert-dialog';
17
+ import {
18
+ Card,
19
+ CardContent,
20
+ CardDescription,
21
+ CardHeader,
22
+ CardTitle,
23
+ } from '@/components/ui/card';
24
+ import {
25
+ ChartContainer,
26
+ ChartTooltip,
27
+ ChartTooltipContent,
28
+ } from '@/components/ui/chart';
29
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
30
+ import { Skeleton } from '@/components/ui/skeleton';
31
+ import {
32
+ Download,
33
+ BookOpen,
34
+ Clapperboard,
35
+ Loader2,
36
+ FileText,
37
+ Film,
38
+ HardDrive,
39
+ Image,
40
+ Layers,
41
+ Mic,
42
+ Paperclip,
43
+ Sparkles,
44
+ Target,
45
+ Trash2,
46
+ type LucideIcon,
47
+ Zap,
48
+ } from 'lucide-react';
49
+ import { useTranslations } from 'next-intl';
50
+ import React, { useEffect, useMemo, useState } from 'react';
51
+ import { Cell, Pie, PieChart } from 'recharts';
52
+ import { toast } from 'sonner';
53
+ import { useCourseContentOverviewQuery } from '../_data/use-course-content-overview';
54
+ import { useCourseXpOverviewQuery } from '../_data/use-course-xp-overview';
55
+
56
+ const TYPE_COLORS: Record<string, string> = {
57
+ video: '#0ea5e9',
58
+ questao: '#f97316',
59
+ post: '#22c55e',
60
+ exercicio: '#a855f7',
61
+ };
62
+
63
+ const TYPE_GRADIENTS: Record<
64
+ string,
65
+ { from: string; icon: LucideIcon; label: string }
66
+ > = {
67
+ video: {
68
+ from: 'from-sky-500/12 via-blue-500/6',
69
+ icon: Clapperboard,
70
+ label: 'Vídeo',
71
+ },
72
+ questao: {
73
+ from: 'from-orange-500/12 via-amber-500/6',
74
+ icon: FileText,
75
+ label: 'Questão',
76
+ },
77
+ post: {
78
+ from: 'from-green-500/12 via-emerald-500/6',
79
+ icon: BookOpen,
80
+ label: 'Post',
81
+ },
82
+ exercicio: {
83
+ from: 'from-violet-500/12 via-fuchsia-500/6',
84
+ icon: Target,
85
+ label: 'Exercício',
86
+ },
87
+ };
88
+
89
+ const AREA_COLORS = ['#0f766e', '#0ea5e9', '#14b8a6', '#38bdf8', '#0891b2'];
90
+ const SKILL_COLORS = ['#ea580c', '#f97316', '#fb923c', '#fdba74', '#fed7aa'];
91
+
92
+ const STORAGE_CATEGORY_META: Record<
93
+ string,
94
+ {
95
+ label: string;
96
+ accentClassName: string;
97
+ icon: LucideIcon;
98
+ iconClassName: string;
99
+ iconBackgroundClassName: string;
100
+ }
101
+ > = {
102
+ video_original: {
103
+ label: 'Vídeos originais',
104
+ accentClassName: 'from-sky-500/20 via-blue-500/10 to-transparent',
105
+ icon: Film,
106
+ iconClassName: 'text-sky-600 dark:text-sky-400',
107
+ iconBackgroundClassName: 'bg-sky-500/10',
108
+ },
109
+ video_profile: {
110
+ label: 'Vídeos convertidos',
111
+ accentClassName: 'from-indigo-500/20 via-blue-500/10 to-transparent',
112
+ icon: Clapperboard,
113
+ iconClassName: 'text-indigo-600 dark:text-indigo-400',
114
+ iconBackgroundClassName: 'bg-indigo-500/10',
115
+ },
116
+ lesson_audio: {
117
+ label: 'Áudios',
118
+ accentClassName: 'from-teal-500/20 via-cyan-500/10 to-transparent',
119
+ icon: Mic,
120
+ iconClassName: 'text-teal-600 dark:text-teal-400',
121
+ iconBackgroundClassName: 'bg-teal-500/10',
122
+ },
123
+ extracted_image: {
124
+ label: 'Imagens extraídas',
125
+ accentClassName: 'from-amber-500/20 via-orange-500/10 to-transparent',
126
+ icon: Image,
127
+ iconClassName: 'text-amber-600 dark:text-amber-400',
128
+ iconBackgroundClassName: 'bg-amber-500/10',
129
+ },
130
+ student_download: {
131
+ label: 'Downloads do aluno',
132
+ accentClassName: 'from-violet-500/20 via-fuchsia-500/10 to-transparent',
133
+ icon: Download,
134
+ iconClassName: 'text-violet-600 dark:text-violet-400',
135
+ iconBackgroundClassName: 'bg-violet-500/10',
136
+ },
137
+ supplementary_material: {
138
+ label: 'Materiais de apoio',
139
+ accentClassName: 'from-fuchsia-500/20 via-pink-500/10 to-transparent',
140
+ icon: Paperclip,
141
+ iconClassName: 'text-fuchsia-600 dark:text-fuchsia-400',
142
+ iconBackgroundClassName: 'bg-fuchsia-500/10',
143
+ },
144
+ course_image: {
145
+ label: 'Imagens do curso',
146
+ accentClassName: 'from-rose-500/20 via-pink-500/10 to-transparent',
147
+ icon: Image,
148
+ iconClassName: 'text-rose-600 dark:text-rose-400',
149
+ iconBackgroundClassName: 'bg-rose-500/10',
150
+ },
151
+ course_file: {
152
+ label: 'Arquivos do curso',
153
+ accentClassName: 'from-slate-500/20 via-zinc-500/10 to-transparent',
154
+ icon: FileText,
155
+ iconClassName: 'text-slate-600 dark:text-slate-400',
156
+ iconBackgroundClassName: 'bg-slate-500/10',
157
+ },
158
+ other_lesson_file: {
159
+ label: 'Outros arquivos',
160
+ accentClassName: 'from-muted/80 via-muted/40 to-transparent',
161
+ icon: HardDrive,
162
+ iconClassName: 'text-muted-foreground',
163
+ iconBackgroundClassName: 'bg-muted',
164
+ },
165
+ };
166
+
167
+ interface Props {
168
+ courseId: string;
169
+ locale: string;
170
+ }
171
+
172
+ type CleanupDialogState = {
173
+ categoryKey: string;
174
+ categoryLabel: string;
175
+ fileCount: number;
176
+ totalBytes: number;
177
+ };
178
+
179
+ type CleanupTrackedJobState = {
180
+ categoryKey: string;
181
+ queueJobId: number;
182
+ status: string;
183
+ };
184
+
185
+ export function CourseOverviewTab({ courseId, locale }: Props) {
186
+ const { request } = useApp();
187
+ const queryClient = useQueryClient();
188
+ const t = useTranslations('lms.CursoEditPage.structureEditor.courseOverview');
189
+ const { data, isLoading, isError } = useCourseContentOverviewQuery(courseId);
190
+ const { data: xpData } = useCourseXpOverviewQuery(courseId);
191
+ const [cleanupCategoryKey, setCleanupCategoryKey] = useState<string | null>(
192
+ null
193
+ );
194
+ const [cleanupDialogState, setCleanupDialogState] =
195
+ useState<CleanupDialogState | null>(null);
196
+ const [cleanupTrackedJob, setCleanupTrackedJob] =
197
+ useState<CleanupTrackedJobState | null>(null);
198
+
199
+ const { data: cleanupTrackedJobDetail, isError: isCleanupTrackedJobError } =
200
+ useQuery<{
201
+ id: number;
202
+ status: string;
203
+ last_error?: string | null;
204
+ } | null>({
205
+ queryKey: [
206
+ 'lms-course-storage-cleanup-job',
207
+ cleanupTrackedJob?.queueJobId,
208
+ ],
209
+ enabled: Boolean(cleanupTrackedJob?.queueJobId),
210
+ queryFn: async () => {
211
+ if (!cleanupTrackedJob?.queueJobId) return null;
212
+ const response = await request<{
213
+ id: number;
214
+ status: string;
215
+ last_error?: string | null;
216
+ }>({
217
+ url: `/queue/jobs/${cleanupTrackedJob.queueJobId}`,
218
+ method: 'GET',
219
+ });
220
+ return response.data;
221
+ },
222
+ refetchInterval: ({ state }) => {
223
+ const status = (state.data as { status?: string } | null)?.status;
224
+ if (!status) return 3000;
225
+ return ['pending', 'scheduled', 'processing', 'retrying'].includes(
226
+ status
227
+ )
228
+ ? 3000
229
+ : false;
230
+ },
231
+ });
232
+
233
+ const formatBytes = (bytes: number) => {
234
+ if (!Number.isFinite(bytes) || bytes <= 0) {
235
+ return '0 B';
236
+ }
237
+
238
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
239
+ let value = bytes;
240
+ let unitIndex = 0;
241
+
242
+ while (value >= 1024 && unitIndex < units.length - 1) {
243
+ value /= 1024;
244
+ unitIndex += 1;
245
+ }
246
+
247
+ const digits = unitIndex === 0 ? 0 : value >= 10 ? 1 : 2;
248
+ return `${new Intl.NumberFormat(locale || 'pt-BR', {
249
+ minimumFractionDigits: 0,
250
+ maximumFractionDigits: digits,
251
+ }).format(value)} ${units[unitIndex]}`;
252
+ };
253
+
254
+ const kpiItems = useMemo(() => {
255
+ if (!data) return [];
256
+ return [
257
+ {
258
+ key: 'modules',
259
+ title: t('kpis.modules'),
260
+ value: data.structure.moduleCount,
261
+ description: t('kpis.modulesDescription'),
262
+ icon: Layers,
263
+ accentClassName: 'from-blue-500 via-indigo-500 to-violet-500',
264
+ iconContainerClassName:
265
+ 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
266
+ },
267
+ {
268
+ key: 'lessons',
269
+ title: t('kpis.lessons'),
270
+ value: data.structure.lessonCount,
271
+ description: t('kpis.lessonsDescription'),
272
+ icon: BookOpen,
273
+ accentClassName: 'from-emerald-500 via-teal-500 to-green-500',
274
+ iconContainerClassName:
275
+ 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
276
+ },
277
+ {
278
+ key: 'images',
279
+ title: t('kpis.extractedImages'),
280
+ value: data.media.extractedImageCount,
281
+ description: t('kpis.extractedImagesDescription'),
282
+ icon: Image,
283
+ accentClassName: 'from-amber-500 via-orange-500 to-yellow-500',
284
+ iconContainerClassName:
285
+ 'bg-amber-500/10 text-amber-600 dark:text-amber-400',
286
+ },
287
+ {
288
+ key: 'resources',
289
+ title: t('kpis.resourceFiles'),
290
+ value: data.resources.fileCount,
291
+ description: t('kpis.resourceFilesDescription'),
292
+ icon: Paperclip,
293
+ accentClassName: 'from-violet-500 via-fuchsia-500 to-purple-500',
294
+ iconContainerClassName:
295
+ 'bg-violet-500/10 text-violet-600 dark:text-violet-400',
296
+ },
297
+ ];
298
+ }, [data, t]);
299
+
300
+ const lessonTypeData = useMemo(() => {
301
+ if (!data) return [];
302
+ const types = ['video', 'questao', 'post', 'exercicio'] as const;
303
+ return types
304
+ .map((type) => ({
305
+ type,
306
+ count: data.structure.lessonsByType[type],
307
+ color: TYPE_COLORS[type],
308
+ label: TYPE_GRADIENTS[type]!.label,
309
+ }))
310
+ .filter((item) => item.count > 0);
311
+ }, [data]);
312
+
313
+ const lessonTypeChartConfig = useMemo(
314
+ () =>
315
+ Object.fromEntries(
316
+ lessonTypeData.map((item) => [
317
+ item.type,
318
+ { label: item.label, color: item.color },
319
+ ])
320
+ ),
321
+ [lessonTypeData]
322
+ );
323
+
324
+ const storageCategories = useMemo(() => {
325
+ if (!data) return [];
326
+
327
+ return data.storage.categories.map((category) => {
328
+ const meta = (STORAGE_CATEGORY_META[category.key] ??
329
+ STORAGE_CATEGORY_META.other_lesson_file)!;
330
+ const sharePercent =
331
+ data.storage.totalBytes > 0
332
+ ? (category.totalBytes / data.storage.totalBytes) * 100
333
+ : 0;
334
+
335
+ return {
336
+ ...category,
337
+ ...meta,
338
+ sharePercent,
339
+ };
340
+ });
341
+ }, [data]);
342
+
343
+ useEffect(() => {
344
+ if (!cleanupTrackedJob || !cleanupTrackedJobDetail) return;
345
+
346
+ const status = cleanupTrackedJobDetail.status;
347
+
348
+ setCleanupTrackedJob((prev) => {
349
+ if (!prev) return prev;
350
+ if (prev.status === status) return prev;
351
+ return { ...prev, status };
352
+ });
353
+
354
+ if (status === 'completed') {
355
+ toast.success('Limpeza concluída com sucesso.');
356
+ void queryClient.invalidateQueries({
357
+ queryKey: ['course-content-overview', courseId],
358
+ });
359
+ setCleanupTrackedJob(null);
360
+ return;
361
+ }
362
+
363
+ if (['failed', 'dead_letter', 'canceled'].includes(status)) {
364
+ toast.error(
365
+ cleanupTrackedJobDetail.last_error ||
366
+ 'A limpeza falhou. Você pode tentar excluir novamente.'
367
+ );
368
+ setCleanupTrackedJob(null);
369
+ }
370
+ }, [cleanupTrackedJob, cleanupTrackedJobDetail, courseId, queryClient]);
371
+
372
+ useEffect(() => {
373
+ if (!cleanupTrackedJob || !isCleanupTrackedJobError) return;
374
+
375
+ toast.error('Não foi possível acompanhar o status da limpeza.');
376
+ setCleanupTrackedJob(null);
377
+ }, [cleanupTrackedJob, isCleanupTrackedJobError]);
378
+
379
+ if (isLoading) {
380
+ return (
381
+ <div className="flex flex-col gap-3">
382
+ <div className="grid grid-cols-2 gap-3 xl:grid-cols-4">
383
+ {Array.from({ length: 4 }).map((_, i) => (
384
+ <Skeleton key={i} className="h-28 rounded-xl" />
385
+ ))}
386
+ </div>
387
+ <div className="grid gap-3 xl:grid-cols-2">
388
+ <Skeleton className="h-72 rounded-xl" />
389
+ <Skeleton className="h-72 rounded-xl" />
390
+ </div>
391
+ </div>
392
+ );
393
+ }
394
+
395
+ if (isError || !data) {
396
+ return (
397
+ <Card className="border-dashed bg-muted/20">
398
+ <CardHeader>
399
+ <CardTitle className="text-sm font-semibold">
400
+ {t('errorTitle')}
401
+ </CardTitle>
402
+ <CardDescription>{t('errorDescription')}</CardDescription>
403
+ </CardHeader>
404
+ </Card>
405
+ );
406
+ }
407
+
408
+ const totalLessons = data.structure.lessonCount;
409
+ const videoCount = data.videos.lessonCount;
410
+ const overviewHighlights = [
411
+ {
412
+ key: 'storage',
413
+ label: 'Armazenamento',
414
+ value: formatBytes(data.storage.totalBytes),
415
+ toneClassName:
416
+ 'border-cyan-500/15 bg-cyan-500/10 text-cyan-700 dark:text-cyan-300',
417
+ },
418
+ {
419
+ key: 'coverage',
420
+ label: 'Cobertura de vídeo',
421
+ value:
422
+ totalLessons > 0
423
+ ? `${Math.round((videoCount / totalLessons) * 100)}% das aulas`
424
+ : '0% das aulas',
425
+ toneClassName:
426
+ 'border-teal-500/15 bg-teal-500/10 text-teal-700 dark:text-teal-300',
427
+ },
428
+ {
429
+ key: 'images',
430
+ label: 'Imagens extraídas',
431
+ value: `${data.media.extractedImageCount}`,
432
+ toneClassName:
433
+ 'border-amber-500/15 bg-amber-500/10 text-amber-700 dark:text-amber-300',
434
+ },
435
+ {
436
+ key: 'resources',
437
+ label: 'Arquivos de apoio',
438
+ value: `${data.resources.fileCount}`,
439
+ toneClassName:
440
+ 'border-violet-500/15 bg-violet-500/10 text-violet-700 dark:text-violet-300',
441
+ },
442
+ ];
443
+
444
+ const openCleanupCategoryDialog = (
445
+ categoryKey: string,
446
+ categoryLabel: string,
447
+ fileCount: number,
448
+ totalBytes: number
449
+ ) => {
450
+ if (fileCount <= 0 || cleanupCategoryKey) {
451
+ return;
452
+ }
453
+
454
+ setCleanupDialogState({
455
+ categoryKey,
456
+ categoryLabel,
457
+ fileCount,
458
+ totalBytes,
459
+ });
460
+ };
461
+
462
+ const handleCleanupCategory = async () => {
463
+ if (!cleanupDialogState) {
464
+ return;
465
+ }
466
+
467
+ const { categoryKey, categoryLabel } = cleanupDialogState;
468
+
469
+ setCleanupDialogState(null);
470
+
471
+ setCleanupCategoryKey(categoryKey);
472
+
473
+ try {
474
+ const response = await request<{
475
+ status?: string;
476
+ queueJobId?: number;
477
+ queueJobStatus?: string;
478
+ cleanupImpact?: { formattedSize?: string };
479
+ }>({
480
+ url: `/lms/courses/${courseId}/storage/cleanup`,
481
+ method: 'POST',
482
+ data: { category: categoryKey },
483
+ });
484
+
485
+ if (response.data?.status === 'empty') {
486
+ toast.info(
487
+ `A categoria "${categoryLabel}" não possui arquivos para limpeza.`
488
+ );
489
+ } else {
490
+ if (response.data?.queueJobId) {
491
+ setCleanupTrackedJob({
492
+ categoryKey,
493
+ queueJobId: response.data.queueJobId,
494
+ status: response.data.queueJobStatus || 'pending',
495
+ });
496
+ }
497
+ toast.success(
498
+ `Limpeza da categoria "${categoryLabel}" enfileirada com sucesso.`
499
+ );
500
+ }
501
+
502
+ await queryClient.invalidateQueries({
503
+ queryKey: ['course-content-overview', courseId],
504
+ });
505
+ } catch (error: unknown) {
506
+ const responseMessage =
507
+ typeof error === 'object' &&
508
+ error !== null &&
509
+ 'response' in error &&
510
+ typeof (error as { response?: { data?: { message?: unknown } } })
511
+ .response?.data?.message === 'string'
512
+ ? (error as { response?: { data?: { message?: string } } }).response
513
+ ?.data?.message
514
+ : undefined;
515
+ const message =
516
+ responseMessage ||
517
+ (error instanceof Error ? error.message : undefined) ||
518
+ 'Não foi possível enfileirar a limpeza da categoria.';
519
+ toast.error(String(message));
520
+ } finally {
521
+ setCleanupCategoryKey(null);
522
+ }
523
+ };
524
+
525
+ return (
526
+ <>
527
+ <AlertDialog
528
+ open={Boolean(cleanupDialogState)}
529
+ onOpenChange={(open) => {
530
+ if (!open && !cleanupCategoryKey) {
531
+ setCleanupDialogState(null);
532
+ }
533
+ }}
534
+ >
535
+ <AlertDialogContent>
536
+ <AlertDialogHeader>
537
+ <AlertDialogTitle>Confirmar exclusão da categoria</AlertDialogTitle>
538
+ <AlertDialogDescription>
539
+ {cleanupDialogState
540
+ ? `Confirma excluir todos os ${cleanupDialogState.fileCount} arquivo(s) da categoria "${cleanupDialogState.categoryLabel}" deste curso?`
541
+ : 'Confirme a exclusão dos arquivos desta categoria.'}
542
+ {cleanupDialogState
543
+ ? ` Espaço estimado a liberar: ${formatBytes(cleanupDialogState.totalBytes)}.`
544
+ : ''}{' '}
545
+ A remoção será processada em background via fila.
546
+ </AlertDialogDescription>
547
+ </AlertDialogHeader>
548
+ <AlertDialogFooter>
549
+ <AlertDialogCancel disabled={cleanupCategoryKey !== null}>
550
+ Cancelar
551
+ </AlertDialogCancel>
552
+ <AlertDialogAction
553
+ disabled={cleanupCategoryKey !== null}
554
+ onClick={(event) => {
555
+ event.preventDefault();
556
+ void handleCleanupCategory();
557
+ }}
558
+ className="bg-red-600 text-white hover:bg-red-700"
559
+ >
560
+ {cleanupCategoryKey !== null ? 'Enfileirando...' : 'Excluir'}
561
+ </AlertDialogAction>
562
+ </AlertDialogFooter>
563
+ </AlertDialogContent>
564
+ </AlertDialog>
565
+
566
+ <div className="flex flex-col gap-2.5">
567
+ <div className="rounded-2xl border border-border/70 bg-linear-to-br from-background via-background to-muted/30 px-2.5 py-2 shadow-[0_14px_34px_-28px_rgba(15,23,42,0.45)] backdrop-blur-sm sm:px-3 sm:py-2.5">
568
+ <div className="flex flex-wrap items-start justify-between gap-3 border-b border-border/50 pb-2">
569
+ <div className="min-w-0">
570
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-cyan-700/80 dark:text-cyan-300/80">
571
+ Course overview
572
+ </div>
573
+ <div className="mt-1 text-sm font-semibold text-foreground">
574
+ Panorama estrutural e de mídia do curso
575
+ </div>
576
+ <div className="mt-0.5 text-xs text-muted-foreground">
577
+ Visão consolidada de módulos, aulas, tipos de conteúdo, vídeo e
578
+ apoio.
579
+ </div>
580
+ </div>
581
+ <div className="flex flex-wrap gap-1.5">
582
+ {overviewHighlights.map((item) => (
583
+ <div
584
+ key={item.key}
585
+ className={`inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-xs shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md ${item.toneClassName}`}
586
+ >
587
+ <span className="text-[10px] font-semibold uppercase tracking-[0.14em] opacity-80">
588
+ {item.label}
589
+ </span>
590
+ <span className="font-semibold">{item.value}</span>
591
+ </div>
592
+ ))}
593
+ </div>
594
+ </div>
595
+
596
+ <div className="mt-2">
597
+ <KpiCardsGrid
598
+ items={kpiItems.map((item) => ({
599
+ ...item,
600
+ layout: 'compact' as const,
601
+ }))}
602
+ columns={4}
603
+ className="gap-2.5"
604
+ cardClassName="shadow-[0_12px_24px_-22px_rgba(15,23,42,0.45)] transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_18px_34px_-22px_rgba(15,23,42,0.5)]"
605
+ />
606
+ </div>
607
+ </div>
608
+
609
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
610
+ <CardHeader className="border-b border-border/70 bg-linear-to-r from-cyan-500/12 via-sky-500/6 to-transparent pt-2.5 pb-1.5!">
611
+ <div className="flex flex-wrap items-start justify-between gap-3">
612
+ <div className="flex min-w-0 items-start gap-3">
613
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-cyan-500/10 text-cyan-600 ring-1 ring-inset ring-cyan-500/15 dark:text-cyan-400">
614
+ <HardDrive className="size-4" />
615
+ </div>
616
+ <div className="min-w-0">
617
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-cyan-600/80 dark:text-cyan-400/80">
618
+ Storage footprint
619
+ </div>
620
+ <CardTitle className="mt-1 text-sm font-semibold">
621
+ Uso de armazenamento do curso
622
+ </CardTitle>
623
+ <CardDescription className="mt-0.5">
624
+ Soma dos arquivos do conteúdo do curso, com detalhamento por
625
+ categoria.
626
+ </CardDescription>
627
+ </div>
628
+ </div>
629
+ <div className="rounded-full border border-cyan-500/15 bg-cyan-500/10 px-2.5 py-1 text-[10px] font-semibold text-cyan-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-cyan-500/15 hover:shadow-md dark:text-cyan-300">
630
+ {formatBytes(data.storage.totalBytes)}
631
+ </div>
632
+ </div>
633
+ </CardHeader>
634
+ <CardContent className="grid gap-3 px-2.5 py-2.5 sm:px-3 xl:grid-cols-[minmax(0,240px)_minmax(0,1fr)]">
635
+ <div className="grid gap-2">
636
+ <div className="rounded-2xl border border-cyan-500/15 bg-linear-to-br from-cyan-500/12 via-sky-500/6 to-transparent px-3 py-3 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
637
+ <div className="text-[10px] font-semibold uppercase tracking-[0.18em] text-cyan-700/80 dark:text-cyan-300/80">
638
+ Total armazenado
639
+ </div>
640
+ <div className="mt-2 text-2xl font-semibold tracking-tight text-foreground">
641
+ {formatBytes(data.storage.totalBytes)}
642
+ </div>
643
+ <div className="mt-1 text-xs text-muted-foreground">
644
+ {data.storage.totalFileCount} arquivos relacionados ao curso
645
+ </div>
646
+ </div>
647
+
648
+ <div className="grid grid-cols-2 gap-2">
649
+ <div className="rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:bg-background/95 hover:shadow-sm">
650
+ <div className="text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
651
+ Categorias
652
+ </div>
653
+ <div className="mt-1 text-lg font-semibold text-foreground">
654
+ {storageCategories.length}
655
+ </div>
656
+ </div>
657
+ <div className="rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:bg-background/95 hover:shadow-sm">
658
+ <div className="text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
659
+ Maior bloco
660
+ </div>
661
+ <div className="mt-1 truncate text-sm font-semibold text-foreground">
662
+ {storageCategories[0]?.label ?? 'Nenhum'}
663
+ </div>
664
+ </div>
665
+ </div>
666
+ </div>
667
+
668
+ <div className="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-2">
669
+ {storageCategories.length > 0 ? (
670
+ storageCategories.map((category) => {
671
+ const Icon = category.icon;
672
+
673
+ return (
674
+ <div
675
+ key={category.key}
676
+ className="overflow-hidden rounded-2xl border border-border/60 bg-card/90 transition-all duration-200 hover:-translate-y-0.5 hover:border-cyan-500/20 hover:bg-background/95 hover:shadow-md"
677
+ >
678
+ <div
679
+ className={`h-1.5 w-full bg-linear-to-r ${category.accentClassName}`}
680
+ />
681
+ <div className="flex flex-col gap-3 px-3 py-3">
682
+ <div className="flex items-start gap-3">
683
+ <div
684
+ className={`flex size-9 shrink-0 items-center justify-center rounded-xl ${category.iconBackgroundClassName}`}
685
+ >
686
+ <Icon
687
+ className={`size-4 ${category.iconClassName}`}
688
+ />
689
+ </div>
690
+ <div className="min-w-0 flex-1">
691
+ <div className="text-sm font-semibold text-foreground">
692
+ {category.label}
693
+ </div>
694
+ <div className="mt-0.5 text-xs text-muted-foreground">
695
+ {category.fileCount}{' '}
696
+ {category.fileCount === 1
697
+ ? 'arquivo'
698
+ : 'arquivos'}
699
+ </div>
700
+ </div>
701
+ <div className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[10px] font-semibold text-muted-foreground">
702
+ {category.sharePercent.toFixed(
703
+ category.sharePercent >= 10 ? 0 : 1
704
+ )}
705
+ %
706
+ </div>
707
+ </div>
708
+
709
+ <div className="flex items-center justify-between gap-2">
710
+ <span className="text-[11px] text-muted-foreground">
711
+ Limpar categoria para liberar espaço
712
+ </span>
713
+ {cleanupTrackedJob?.categoryKey === category.key &&
714
+ [
715
+ 'pending',
716
+ 'scheduled',
717
+ 'processing',
718
+ 'retrying',
719
+ ].includes(cleanupTrackedJob.status) ? (
720
+ <Badge
721
+ variant="secondary"
722
+ className="h-7 gap-1.5 rounded-lg border border-amber-500/20 bg-amber-500/10 px-2.5 text-[11px] font-semibold text-amber-700 dark:text-amber-300"
723
+ >
724
+ <Loader2 className="size-3 animate-spin" />
725
+ Carregando...
726
+ </Badge>
727
+ ) : (
728
+ <Button
729
+ type="button"
730
+ size="sm"
731
+ variant="outline"
732
+ className="h-7 gap-1.5 rounded-lg border-red-500/30 bg-red-500/5 px-2.5 text-[11px] font-semibold text-red-700 transition-all duration-200 hover:-translate-y-0.5 hover:bg-red-500/10 hover:text-red-800 dark:text-red-300 dark:hover:text-red-200"
733
+ disabled={
734
+ cleanupCategoryKey !== null ||
735
+ category.fileCount <= 0
736
+ }
737
+ onClick={() =>
738
+ openCleanupCategoryDialog(
739
+ category.key,
740
+ category.label,
741
+ category.fileCount,
742
+ category.totalBytes
743
+ )
744
+ }
745
+ >
746
+ {cleanupCategoryKey === category.key ? (
747
+ <>
748
+ <Loader2 className="size-3 animate-spin" />
749
+ Enfileirando...
750
+ </>
751
+ ) : (
752
+ <>
753
+ <Trash2 className="size-3" />
754
+ Excluir
755
+ </>
756
+ )}
757
+ </Button>
758
+ )}
759
+ </div>
760
+
761
+ <div>
762
+ <div className="flex items-baseline justify-between gap-3">
763
+ <span className="text-lg font-semibold tracking-tight text-foreground">
764
+ {formatBytes(category.totalBytes)}
765
+ </span>
766
+ <span className="text-xs text-muted-foreground">
767
+ de {formatBytes(data.storage.totalBytes)}
768
+ </span>
769
+ </div>
770
+ <div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-muted/80">
771
+ <div
772
+ className="h-full rounded-full transition-all duration-700 ease-out"
773
+ style={{
774
+ width: `${Math.max(category.sharePercent, category.totalBytes > 0 ? 2 : 0)}%`,
775
+ background: `linear-gradient(90deg, rgba(34,211,238,0.9), rgba(14,165,233,0.75))`,
776
+ }}
777
+ />
778
+ </div>
779
+ </div>
780
+ </div>
781
+ </div>
782
+ );
783
+ })
784
+ ) : (
785
+ <div className="flex min-h-32 items-center justify-center rounded-2xl border border-dashed border-border/70 bg-muted/15 px-4 py-6 text-center text-sm text-muted-foreground">
786
+ Nenhum arquivo relacionado ao curso foi encontrado.
787
+ </div>
788
+ )}
789
+ </div>
790
+ </CardContent>
791
+ </Card>
792
+
793
+ <div className="grid gap-3 xl:grid-cols-2">
794
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
795
+ <CardHeader className="border-b border-border/70 bg-linear-to-r from-sky-500/12 via-blue-500/6 to-transparent pt-2.5 pb-1.5!">
796
+ <div className="flex flex-wrap items-start justify-between gap-3">
797
+ <div className="flex min-w-0 items-start gap-3">
798
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-sky-500/10 text-sky-600 ring-1 ring-inset ring-sky-500/15 dark:text-sky-400">
799
+ <Layers className="size-4" />
800
+ </div>
801
+ <div className="min-w-0">
802
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-600/80 dark:text-sky-400/80">
803
+ Lesson mix
804
+ </div>
805
+ <CardTitle className="mt-1 text-sm font-semibold">
806
+ {t('lessonTypes.title')}
807
+ </CardTitle>
808
+ <CardDescription className="mt-0.5">
809
+ {t('lessonTypes.description')}
810
+ </CardDescription>
811
+ </div>
812
+ </div>
813
+ <div className="rounded-full border border-sky-500/15 bg-sky-500/10 px-2.5 py-1 text-[10px] font-semibold text-sky-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-sky-500/15 hover:shadow-md dark:text-sky-300">
814
+ {totalLessons} aulas
815
+ </div>
816
+ </div>
817
+ </CardHeader>
818
+ <CardContent className="grid gap-2.5 px-2.5 py-2.5 sm:px-3 md:grid-cols-[minmax(0,200px)_minmax(0,1fr)] md:items-start">
819
+ {lessonTypeData.length > 0 ? (
820
+ <ChartContainer
821
+ config={lessonTypeChartConfig}
822
+ className="h-48 w-full"
823
+ >
824
+ <PieChart>
825
+ <Pie
826
+ data={lessonTypeData}
827
+ dataKey="count"
828
+ nameKey="label"
829
+ innerRadius={52}
830
+ outerRadius={80}
831
+ paddingAngle={3}
832
+ animationDuration={900}
833
+ strokeWidth={3}
834
+ stroke="hsl(var(--background))"
835
+ >
836
+ {lessonTypeData.map((entry) => (
837
+ <Cell key={entry.type} fill={entry.color} />
838
+ ))}
839
+ </Pie>
840
+ <ChartTooltip
841
+ content={
842
+ <ChartTooltipContent
843
+ hideIndicator
844
+ formatter={(value, _name, item) => {
845
+ const payload = item?.payload as
846
+ | { label?: string; count?: number }
847
+ | undefined;
848
+ const pct =
849
+ totalLessons > 0
850
+ ? (
851
+ ((payload?.count ?? 0) / totalLessons) *
852
+ 100
853
+ ).toFixed(1)
854
+ : '0';
855
+ return (
856
+ <div className="flex min-w-36 flex-col gap-1 text-xs">
857
+ <span className="font-medium text-foreground">
858
+ {payload?.label}
859
+ </span>
860
+ <span className="text-muted-foreground">
861
+ {value} aulas · {pct}%
862
+ </span>
863
+ </div>
864
+ );
865
+ }}
866
+ />
867
+ }
868
+ />
869
+ </PieChart>
870
+ </ChartContainer>
871
+ ) : (
872
+ <div className="flex h-48 items-center justify-center text-sm text-muted-foreground">
873
+ Nenhuma aula
874
+ </div>
875
+ )}
876
+
877
+ <div className="grid gap-2">
878
+ {lessonTypeData.map((item) => {
879
+ const Icon = TYPE_GRADIENTS[item.type]!.icon;
880
+ const pct =
881
+ totalLessons > 0
882
+ ? ((item.count / totalLessons) * 100).toFixed(1)
883
+ : '0';
884
+ return (
885
+ <div
886
+ key={item.type}
887
+ className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-sky-500/20 hover:bg-background/95 hover:shadow-md"
888
+ >
889
+ <div
890
+ className="flex size-8 shrink-0 items-center justify-center rounded-lg"
891
+ style={{ backgroundColor: `${item.color}20` }}
892
+ >
893
+ <Icon
894
+ className="size-4"
895
+ style={{ color: item.color }}
896
+ />
897
+ </div>
898
+ <div className="min-w-0 flex-1">
899
+ <div className="flex items-center justify-between">
900
+ <span className="text-sm font-medium text-foreground">
901
+ {item.label}
902
+ </span>
903
+ <span className="font-mono text-xs text-muted-foreground transition-colors duration-200 group-hover:text-foreground">
904
+ {pct}%
905
+ </span>
906
+ </div>
907
+ <div className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-muted">
908
+ <div
909
+ className="h-full rounded-full transition-all duration-700"
910
+ style={{
911
+ width: `${pct}%`,
912
+ backgroundColor: item.color,
913
+ }}
914
+ />
915
+ </div>
916
+ <div className="mt-1 text-xs text-muted-foreground">
917
+ {item.count} {item.count === 1 ? 'aula' : 'aulas'}
918
+ </div>
919
+ </div>
920
+ </div>
921
+ );
922
+ })}
923
+ </div>
924
+ </CardContent>
925
+ </Card>
926
+
927
+ {/* Video coverage */}
928
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
929
+ <CardHeader className="border-b border-border/70 bg-linear-to-r from-teal-500/12 via-cyan-500/6 to-transparent pt-2.5 pb-1.5!">
930
+ <div className="flex flex-wrap items-start justify-between gap-3">
931
+ <div className="flex min-w-0 items-start gap-3">
932
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-teal-500/10 text-teal-600 ring-1 ring-inset ring-teal-500/15 dark:text-teal-400">
933
+ <Clapperboard className="size-4" />
934
+ </div>
935
+ <div className="min-w-0">
936
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-teal-600/80 dark:text-teal-400/80">
937
+ Video coverage
938
+ </div>
939
+ <CardTitle className="mt-1 text-sm font-semibold">
940
+ {t('videos.title')}
941
+ </CardTitle>
942
+ <CardDescription className="mt-0.5">
943
+ {t('videos.description')}
944
+ </CardDescription>
945
+ </div>
946
+ </div>
947
+ <div className="rounded-full border border-teal-500/15 bg-teal-500/10 px-2.5 py-1 text-[10px] font-semibold text-teal-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-teal-500/15 hover:shadow-md dark:text-teal-300">
948
+ {videoCount} vídeos
949
+ </div>
950
+ </div>
951
+ </CardHeader>
952
+ <CardContent className="flex flex-col gap-3 px-2.5 py-2.5 sm:px-3">
953
+ {(
954
+ [
955
+ {
956
+ key: 'total',
957
+ label: t('videos.total'),
958
+ value: videoCount,
959
+ total: videoCount,
960
+ color: '#0ea5e9',
961
+ bgColor: 'bg-sky-500/10',
962
+ textColor: 'text-sky-600 dark:text-sky-400',
963
+ icon: Clapperboard,
964
+ },
965
+ {
966
+ key: 'transcription',
967
+ label: t('videos.withTranscription'),
968
+ value: data.videos.withTranscription,
969
+ total: videoCount,
970
+ color: '#14b8a6',
971
+ bgColor: 'bg-teal-500/10',
972
+ textColor: 'text-teal-600 dark:text-teal-400',
973
+ icon: Mic,
974
+ },
975
+ {
976
+ key: 'xp',
977
+ label: t('videos.withXp'),
978
+ value: data.videos.withXp,
979
+ total: videoCount,
980
+ color: '#8b5cf6',
981
+ bgColor: 'bg-violet-500/10',
982
+ textColor: 'text-violet-600 dark:text-violet-400',
983
+ icon: Zap,
984
+ },
985
+ ] as const
986
+ ).map((stat) => {
987
+ const pct =
988
+ stat.total > 0
989
+ ? Math.round((stat.value / stat.total) * 100)
990
+ : stat.key === 'total'
991
+ ? 100
992
+ : 0;
993
+ const Icon = stat.icon;
994
+ return (
995
+ <div
996
+ key={stat.key}
997
+ className="flex flex-col gap-2 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 hover:-translate-y-0.5 hover:border-border/60 hover:bg-background/70 hover:shadow-sm"
998
+ >
999
+ <div className="flex items-center gap-3">
1000
+ <div
1001
+ className={`flex size-8 shrink-0 items-center justify-center rounded-lg ${stat.bgColor}`}
1002
+ >
1003
+ <Icon className={`size-4 ${stat.textColor}`} />
1004
+ </div>
1005
+ <div className="flex flex-1 items-center justify-between">
1006
+ <span className="text-sm font-medium text-foreground">
1007
+ {stat.label}
1008
+ </span>
1009
+ <div className="flex items-center gap-2">
1010
+ <span className="font-mono text-xs text-muted-foreground">
1011
+ {stat.value}/{stat.total}
1012
+ </span>
1013
+ <span
1014
+ className="rounded-full px-2 py-0.5 text-[10px] font-semibold transition-all duration-200 hover:shadow-sm"
1015
+ style={{
1016
+ backgroundColor: `${stat.color}20`,
1017
+ color: stat.color,
1018
+ }}
1019
+ >
1020
+ {pct}%
1021
+ </span>
1022
+ </div>
1023
+ </div>
1024
+ </div>
1025
+ <div className="ml-11 h-2 w-full overflow-hidden rounded-full bg-muted">
1026
+ <div
1027
+ className="h-full rounded-full transition-all duration-700 ease-out"
1028
+ style={{
1029
+ width: `${pct}%`,
1030
+ backgroundColor: stat.color,
1031
+ }}
1032
+ />
1033
+ </div>
1034
+ </div>
1035
+ );
1036
+ })}
1037
+
1038
+ {videoCount === 0 && (
1039
+ <p className="text-center text-sm text-muted-foreground">
1040
+ Nenhuma aula de vídeo neste curso
1041
+ </p>
1042
+ )}
1043
+ </CardContent>
1044
+ </Card>
1045
+ </div>
1046
+
1047
+ {/* Areas and Skills (conditional on XP data) */}
1048
+ {xpData && (xpData.areas.length > 0 || xpData.skills.length > 0) && (
1049
+ <div className="grid gap-3 xl:grid-cols-2">
1050
+ {xpData.areas.length > 0 && (
1051
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
1052
+ <CardHeader className="border-b border-border/70 bg-linear-to-r from-teal-500/10 via-cyan-500/5 to-transparent pt-2.5 pb-1.5!">
1053
+ <div className="flex flex-wrap items-start justify-between gap-3">
1054
+ <div className="flex min-w-0 items-start gap-3">
1055
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-teal-500/10 text-teal-600 ring-1 ring-inset ring-teal-500/15 dark:text-teal-400">
1056
+ <Sparkles className="size-4" />
1057
+ </div>
1058
+ <div className="min-w-0">
1059
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-teal-600/80 dark:text-teal-400/80">
1060
+ Macro areas
1061
+ </div>
1062
+ <CardTitle className="mt-1 text-sm font-semibold">
1063
+ {t('areas.title')}
1064
+ </CardTitle>
1065
+ <CardDescription className="mt-0.5">
1066
+ {t('areas.description')}
1067
+ </CardDescription>
1068
+ </div>
1069
+ </div>
1070
+ <div className="rounded-full border border-teal-500/15 bg-teal-500/10 px-2.5 py-1 text-[10px] font-semibold text-teal-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-teal-500/15 hover:shadow-md dark:text-teal-300">
1071
+ {xpData.areas.length} áreas
1072
+ </div>
1073
+ </div>
1074
+ </CardHeader>
1075
+ <CardContent className="flex flex-col gap-2 px-2.5 py-2.5 sm:px-3">
1076
+ {xpData.areas.map((area, index) => (
1077
+ <div
1078
+ key={area.xpAreaId}
1079
+ className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-teal-500/20 hover:bg-background/95 hover:shadow-md"
1080
+ >
1081
+ <div
1082
+ className="flex size-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold text-white"
1083
+ style={{
1084
+ backgroundColor:
1085
+ AREA_COLORS[index % AREA_COLORS.length],
1086
+ }}
1087
+ >
1088
+ {index + 1}
1089
+ </div>
1090
+ <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
1091
+ {area.name}
1092
+ </span>
1093
+ <span
1094
+ className="rounded-full px-2 py-0.5 text-[10px] font-semibold transition-all duration-200 hover:shadow-sm"
1095
+ style={{
1096
+ backgroundColor: `${AREA_COLORS[index % AREA_COLORS.length]}20`,
1097
+ color: AREA_COLORS[index % AREA_COLORS.length],
1098
+ }}
1099
+ >
1100
+ {area.sharePercent.toFixed(1)}%
1101
+ </span>
1102
+ </div>
1103
+ ))}
1104
+ </CardContent>
1105
+ </Card>
1106
+ )}
1107
+
1108
+ {xpData.skills.length > 0 && (
1109
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
1110
+ <CardHeader className="border-b border-border/70 bg-linear-to-r from-orange-500/10 via-amber-500/5 to-transparent pt-2.5 pb-1.5!">
1111
+ <div className="flex flex-wrap items-start justify-between gap-3">
1112
+ <div className="flex min-w-0 items-start gap-3">
1113
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-orange-500/10 text-orange-600 ring-1 ring-inset ring-orange-500/15 dark:text-orange-400">
1114
+ <Target className="size-4" />
1115
+ </div>
1116
+ <div className="min-w-0">
1117
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-orange-600/80 dark:text-orange-400/80">
1118
+ Skills
1119
+ </div>
1120
+ <CardTitle className="mt-1 text-sm font-semibold">
1121
+ {t('skills.title')}
1122
+ </CardTitle>
1123
+ <CardDescription className="mt-0.5">
1124
+ {t('skills.description')}
1125
+ </CardDescription>
1126
+ </div>
1127
+ </div>
1128
+ <div className="rounded-full border border-orange-500/15 bg-orange-500/10 px-2.5 py-1 text-[10px] font-semibold text-orange-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-orange-500/15 hover:shadow-md dark:text-orange-300">
1129
+ {xpData.skills.length} skills
1130
+ </div>
1131
+ </div>
1132
+ </CardHeader>
1133
+ <CardContent className="flex flex-col gap-2 px-2.5 py-2.5 sm:px-3">
1134
+ {xpData.skills.map((skill, index) => (
1135
+ <div
1136
+ key={skill.xpSkillId}
1137
+ className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-orange-500/20 hover:bg-background/95 hover:shadow-md"
1138
+ >
1139
+ <div
1140
+ className="flex size-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold text-white"
1141
+ style={{
1142
+ backgroundColor:
1143
+ SKILL_COLORS[index % SKILL_COLORS.length],
1144
+ }}
1145
+ >
1146
+ {index + 1}
1147
+ </div>
1148
+ <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
1149
+ {skill.name}
1150
+ </span>
1151
+ <span
1152
+ className="rounded-full px-2 py-0.5 text-[10px] font-semibold transition-all duration-200 hover:shadow-sm"
1153
+ style={{
1154
+ backgroundColor: `${SKILL_COLORS[index % SKILL_COLORS.length]}20`,
1155
+ color: SKILL_COLORS[index % SKILL_COLORS.length],
1156
+ }}
1157
+ >
1158
+ {skill.sharePercent.toFixed(1)}%
1159
+ </span>
1160
+ </div>
1161
+ ))}
1162
+ </CardContent>
1163
+ </Card>
1164
+ )}
1165
+ </div>
1166
+ )}
1167
+ </div>
1168
+ </>
1169
+ );
1170
+ }