@hed-hog/lms 0.0.306 → 0.0.309

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 (117) hide show
  1. package/dist/course/course-structure.controller.d.ts +60 -0
  2. package/dist/course/course-structure.controller.d.ts.map +1 -1
  3. package/dist/course/course-structure.controller.js +79 -0
  4. package/dist/course/course-structure.controller.js.map +1 -1
  5. package/dist/course/course-structure.service.d.ts +61 -1
  6. package/dist/course/course-structure.service.d.ts.map +1 -1
  7. package/dist/course/course-structure.service.js +326 -1
  8. package/dist/course/course-structure.service.js.map +1 -1
  9. package/dist/course/course.controller.d.ts +52 -4
  10. package/dist/course/course.controller.d.ts.map +1 -1
  11. package/dist/course/course.service.d.ts +52 -5
  12. package/dist/course/course.service.d.ts.map +1 -1
  13. package/dist/course/course.service.js +78 -57
  14. package/dist/course/course.service.js.map +1 -1
  15. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  16. package/dist/course/dto/create-course-structure-lesson.dto.js +5 -1
  17. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  18. package/dist/course/dto/create-course.dto.d.ts +1 -1
  19. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  20. package/dist/course/dto/create-course.dto.js +4 -1
  21. package/dist/course/dto/create-course.dto.js.map +1 -1
  22. package/dist/course/dto/move-lesson.dto.d.ts +10 -0
  23. package/dist/course/dto/move-lesson.dto.d.ts.map +1 -0
  24. package/dist/course/dto/move-lesson.dto.js +28 -0
  25. package/dist/course/dto/move-lesson.dto.js.map +1 -0
  26. package/dist/course/dto/paste-lessons.dto.d.ts +4 -0
  27. package/dist/course/dto/paste-lessons.dto.d.ts.map +1 -0
  28. package/dist/course/dto/paste-lessons.dto.js +24 -0
  29. package/dist/course/dto/paste-lessons.dto.js.map +1 -0
  30. package/dist/course/dto/reorder-lessons.dto.d.ts +5 -0
  31. package/dist/course/dto/reorder-lessons.dto.d.ts.map +1 -0
  32. package/dist/course/dto/reorder-lessons.dto.js +24 -0
  33. package/dist/course/dto/reorder-lessons.dto.js.map +1 -0
  34. package/dist/course/dto/reorder-sessions.dto.d.ts +5 -0
  35. package/dist/course/dto/reorder-sessions.dto.d.ts.map +1 -0
  36. package/dist/course/dto/reorder-sessions.dto.js +24 -0
  37. package/dist/course/dto/reorder-sessions.dto.js.map +1 -0
  38. package/dist/training/training.controller.js +1 -1
  39. package/dist/training/training.controller.js.map +1 -1
  40. package/hedhog/data/image_type.yaml +20 -0
  41. package/hedhog/data/menu.yaml +2 -2
  42. package/hedhog/data/route.yaml +60 -6
  43. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +146 -165
  44. package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +70 -0
  45. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +372 -22
  46. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +437 -77
  47. package/hedhog/frontend/app/classes/page.tsx.ejs +311 -289
  48. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +10 -7
  49. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +23 -32
  50. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +3 -9
  51. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +26 -16
  52. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +19 -5
  53. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +10 -14
  54. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +131 -107
  55. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +10 -7
  56. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +38 -19
  57. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +1 -1
  58. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
  59. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +336 -1057
  60. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +45 -0
  61. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -0
  62. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -0
  63. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +64 -0
  64. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
  65. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
  66. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
  67. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -0
  68. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
  69. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -0
  70. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +52 -0
  71. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -0
  72. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -0
  73. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1827 -0
  74. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -0
  75. package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +41 -0
  76. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +184 -0
  77. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -0
  78. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +96 -0
  79. package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +74 -0
  80. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -0
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +948 -0
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -0
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +150 -0
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +182 -0
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +52 -0
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -0
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -0
  89. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -0
  90. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +122 -0
  91. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -0
  92. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +97 -0
  93. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +347 -0
  94. package/hedhog/frontend/app/courses/[id]/structure/_data/course-structure-contract.ts.ejs +195 -0
  95. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +420 -0
  96. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +254 -0
  97. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +987 -0
  98. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +86 -0
  99. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure.ts.ejs +160 -0
  100. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -3212
  101. package/hedhog/frontend/app/courses/page.tsx.ejs +45 -26
  102. package/hedhog/frontend/app/{training → paths}/page.tsx.ejs +29 -7
  103. package/hedhog/frontend/messages/en.json +88 -10
  104. package/hedhog/frontend/messages/pt.json +88 -10
  105. package/hedhog/table/course.yaml +1 -1
  106. package/hedhog/table/image_type.yaml +14 -0
  107. package/package.json +7 -7
  108. package/src/course/course-structure.controller.ts +63 -0
  109. package/src/course/course-structure.service.ts +390 -3
  110. package/src/course/course.service.ts +59 -27
  111. package/src/course/dto/create-course-structure-lesson.dto.ts +3 -2
  112. package/src/course/dto/create-course.dto.ts +4 -1
  113. package/src/course/dto/move-lesson.dto.ts +17 -0
  114. package/src/course/dto/paste-lessons.dto.ts +9 -0
  115. package/src/course/dto/reorder-lessons.dto.ts +10 -0
  116. package/src/course/dto/reorder-sessions.dto.ts +10 -0
  117. package/src/training/training.controller.ts +1 -1
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+
5
+ import { useStructureStore } from './store';
6
+ import { TreeContextMenu } from './tree-context-menu';
7
+ import { TreeRowCourse } from './tree-row-course';
8
+ import { TreeRowLesson } from './tree-row-lesson';
9
+ import { TreeRowSession } from './tree-row-session';
10
+ import type { FlatItem, Lesson, Session } from './types';
11
+
12
+ interface TreeRowProps {
13
+ item: FlatItem;
14
+ isActive: boolean;
15
+ isSelected: boolean;
16
+ query: string;
17
+ isMatched: boolean;
18
+ isEffectivelyExpanded: boolean;
19
+ lessonCountMap: Map<string, number>;
20
+ /** Full ordered visible list — required for SHIFT-range selection. */
21
+ visibleItems: FlatItem[];
22
+ }
23
+
24
+ export function TreeRow({
25
+ item,
26
+ isActive,
27
+ isSelected,
28
+ query,
29
+ isMatched,
30
+ isEffectivelyExpanded,
31
+ lessonCountMap,
32
+ visibleItems,
33
+ }: TreeRowProps) {
34
+ const selectItem = useStructureStore((s) => s.selectItem);
35
+ const toggleExpand = useStructureStore((s) => s.toggleExpand);
36
+ const setMobileSheetOpen = useStructureStore((s) => s.setMobileSheetOpen);
37
+
38
+ const handleClick = useCallback(
39
+ (e: React.MouseEvent) => {
40
+ e.stopPropagation();
41
+ const modifiers = {
42
+ ctrl: e.ctrlKey || e.metaKey,
43
+ shift: e.shiftKey,
44
+ };
45
+ selectItem(item.id, item.type, modifiers, visibleItems);
46
+ // Close mobile sheet only on non-session items (sessions expand in-place)
47
+ if (item.type !== 'session' && !modifiers.ctrl && !modifiers.shift) {
48
+ setMobileSheetOpen(false);
49
+ }
50
+ },
51
+ [item.id, item.type, selectItem, setMobileSheetOpen, visibleItems]
52
+ );
53
+
54
+ const handleToggleExpand = useCallback(
55
+ (e: React.MouseEvent) => {
56
+ e.stopPropagation();
57
+ toggleExpand(item.id);
58
+ },
59
+ [item.id, toggleExpand]
60
+ );
61
+
62
+ if (item.type === 'course') {
63
+ return (
64
+ <TreeContextMenu item={item}>
65
+ <TreeRowCourse
66
+ data={item.data}
67
+ isSelected={isSelected}
68
+ isActive={isActive}
69
+ query={query}
70
+ isMatched={isMatched}
71
+ onClick={handleClick}
72
+ />
73
+ </TreeContextMenu>
74
+ );
75
+ }
76
+
77
+ if (item.type === 'session') {
78
+ const lessonCount = lessonCountMap.get(item.id) ?? 0;
79
+ return (
80
+ <TreeContextMenu item={item}>
81
+ <TreeRowSession
82
+ data={item.data as Session}
83
+ lessonCount={lessonCount}
84
+ isExpanded={isEffectivelyExpanded}
85
+ isSelected={isSelected}
86
+ isActive={isActive}
87
+ query={query}
88
+ isMatched={isMatched}
89
+ onClick={handleClick}
90
+ onToggleExpand={handleToggleExpand}
91
+ />
92
+ </TreeContextMenu>
93
+ );
94
+ }
95
+
96
+ return (
97
+ <TreeContextMenu item={item}>
98
+ <TreeRowLesson
99
+ data={item.data as Lesson}
100
+ isSelected={isSelected}
101
+ isActive={isActive}
102
+ query={query}
103
+ isMatched={isMatched}
104
+ onClick={handleClick}
105
+ />
106
+ </TreeContextMenu>
107
+ );
108
+ }
@@ -0,0 +1,122 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // Types — LMS Course Structure TreeView
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+
5
+ export type LessonType = 'video' | 'questao' | 'post' | 'exercicio';
6
+ export type VideoProvider = 'youtube' | 'vimeo' | 'bunny' | 'custom';
7
+ export type ItemType = 'course' | 'session' | 'lesson';
8
+
9
+ export type LessonStatus =
10
+ | 'preparada'
11
+ | 'gravada'
12
+ | 'editada'
13
+ | 'finalizada'
14
+ | 'publicada';
15
+
16
+ export type Visibility = 'publico' | 'privado' | 'restrito';
17
+
18
+ // ── Domain models ─────────────────────────────────────────────────────────────
19
+
20
+ export interface Course {
21
+ id: string;
22
+ name: string;
23
+ title: string;
24
+ description: string;
25
+ slug: string;
26
+ published: boolean;
27
+ }
28
+
29
+ export interface Session {
30
+ id: string;
31
+ code: string;
32
+ title: string;
33
+ description?: string;
34
+ duration: number;
35
+ order: number;
36
+ visibility?: Visibility;
37
+ published?: boolean;
38
+ }
39
+
40
+ export interface Resource {
41
+ id: string;
42
+ name: string;
43
+ size: string;
44
+ type: string;
45
+ public: boolean;
46
+ /** Blob URL (for newly uploaded files) or remote URL for preview/download */
47
+ url?: string;
48
+ }
49
+
50
+ export interface LessonInstructor {
51
+ id: string;
52
+ role?: 'lead' | 'assistant';
53
+ name?: string;
54
+ }
55
+
56
+ export interface Lesson {
57
+ id: string;
58
+ code: string;
59
+ title: string;
60
+ publicDescription: string;
61
+ privateDescription: string;
62
+ type: LessonType;
63
+ duration: number;
64
+ sessionId: string;
65
+ order: number;
66
+ status?: LessonStatus;
67
+ visibility?: Visibility;
68
+ // Video
69
+ videoProvider?: VideoProvider;
70
+ videoUrl?: string;
71
+ autoDuration?: boolean;
72
+ transcription?: string;
73
+ // Questão
74
+ linkedExam?: string;
75
+ // Post
76
+ postContent?: string;
77
+ // Relations
78
+ resources: Resource[];
79
+ instructors?: LessonInstructor[];
80
+ }
81
+
82
+ // ── Flat tree node (used by virtualizer) ──────────────────────────────────────
83
+
84
+ export type FlatItem =
85
+ | { id: string; type: 'course'; depth: 0; data: Course }
86
+ | { id: string; type: 'session'; depth: 1; data: Session }
87
+ | { id: string; type: 'lesson'; depth: 2; data: Lesson };
88
+
89
+ // ── Form value shapes ─────────────────────────────────────────────────────────
90
+
91
+ export type SessionFormValues = {
92
+ code: string;
93
+ title: string;
94
+ description: string;
95
+ duration: number;
96
+ visibility: Visibility;
97
+ published: boolean;
98
+ };
99
+
100
+ export type LessonFormValues = {
101
+ code: string;
102
+ title: string;
103
+ publicDescription: string;
104
+ privateDescription: string;
105
+ type: LessonType;
106
+ duration: number;
107
+ status: LessonStatus;
108
+ visibility: Visibility;
109
+ videoProvider?: VideoProvider;
110
+ videoUrl?: string;
111
+ autoDuration?: boolean;
112
+ linkedExam?: string;
113
+ postContent?: string;
114
+ };
115
+
116
+ export type CourseFormValues = {
117
+ name: string;
118
+ title: string;
119
+ description: string;
120
+ slug: string;
121
+ published: boolean;
122
+ };
@@ -0,0 +1,318 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef } from 'react';
4
+ import { toast } from 'sonner';
5
+
6
+ import type { SearchFilterHandle } from './search-filter';
7
+ import { useStructureStore } from './store';
8
+ import { buildVisibleItems } from './tree-helpers';
9
+
10
+ /**
11
+ * Checks whether keyboard shortcut handling should be suppressed
12
+ * (the user is typing in a form control or rich-text editor).
13
+ */
14
+ function isEditingTarget(el: Element | null): boolean {
15
+ if (!el) return false;
16
+ const tag = (el as HTMLElement).tagName.toLowerCase();
17
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') return true;
18
+ // contenteditable (Tiptap, etc.)
19
+ if ((el as HTMLElement).isContentEditable) return true;
20
+ // shadcn Select portal, combobox, etc. — has [role=combobox] or [role=listbox]
21
+ const role = (el as HTMLElement).getAttribute('role');
22
+ if (role === 'combobox' || role === 'listbox' || role === 'option')
23
+ return true;
24
+ return false;
25
+ }
26
+
27
+ interface UseCourseStructureShortcutsOptions {
28
+ /** Ref to the SearchFilter imperative handle so Ctrl+F / Escape can focus/clear it. */
29
+ searchRef: React.RefObject<SearchFilterHandle | null>;
30
+ /** Ref to the detail panel wrapper div (for Enter key focusing). */
31
+ detailPanelRef: React.RefObject<HTMLDivElement | null>;
32
+ /** Callback to open/close the shortcuts help sheet. */
33
+ onToggleHelp: () => void;
34
+ /** API-backed duplicate (session or lesson based on active item). */
35
+ onDuplicate?: () => void;
36
+ /** API-backed paste (lesson or session clipboard). */
37
+ onPaste?: () => void;
38
+ }
39
+
40
+ export function useCourseStructureShortcuts({
41
+ searchRef,
42
+ detailPanelRef,
43
+ onToggleHelp,
44
+ onDuplicate,
45
+ onPaste,
46
+ }: UseCourseStructureShortcutsOptions) {
47
+ const course = useStructureStore((s) => s.course);
48
+ const sessions = useStructureStore((s) => s.sessions);
49
+ const lessons = useStructureStore((s) => s.lessons);
50
+ const expandedIds = useStructureStore((s) => s.expandedIds);
51
+ const filterQuery = useStructureStore((s) => s.filterQuery);
52
+ const selectedIds = useStructureStore((s) => s.selectedIds);
53
+
54
+ const activeItemId = useStructureStore((s) => s.activeItemId);
55
+ const activeItemType = useStructureStore((s) => s.activeItemType);
56
+
57
+ const selectItem = useStructureStore((s) => s.selectItem);
58
+ const clearSelection = useStructureStore((s) => s.clearSelection);
59
+ const setFilter = useStructureStore((s) => s.setFilter);
60
+ const toggleExpand = useStructureStore((s) => s.toggleExpand);
61
+ const copyItems = useStructureStore((s) => s.copyItems);
62
+ const addSession = useStructureStore((s) => s.addSession);
63
+ const addLesson = useStructureStore((s) => s.addLesson);
64
+ const deleteSession = useStructureStore((s) => s.deleteSession);
65
+ const deleteLesson = useStructureStore((s) => s.deleteLesson);
66
+ const deleteSelected = useStructureStore((s) => s.deleteSelected);
67
+ const duplicateSelected = useStructureStore((s) => s.duplicateSelected);
68
+ const showConfirm = useStructureStore((s) => s.showConfirm);
69
+
70
+ // Keep a stable ref to store values so handlers don't stale-close
71
+ const stateRef = useRef({
72
+ course,
73
+ sessions,
74
+ lessons,
75
+ expandedIds,
76
+ filterQuery,
77
+ selectedIds,
78
+ activeItemId,
79
+ activeItemType,
80
+ selectItem,
81
+ clearSelection,
82
+ setFilter,
83
+ toggleExpand,
84
+ copyItems,
85
+ addSession,
86
+ addLesson,
87
+ deleteSession,
88
+ deleteLesson,
89
+ deleteSelected,
90
+ duplicateSelected,
91
+ showConfirm,
92
+ });
93
+
94
+ useEffect(() => {
95
+ stateRef.current = {
96
+ course,
97
+ sessions,
98
+ lessons,
99
+ expandedIds,
100
+ filterQuery,
101
+ selectedIds,
102
+ activeItemId,
103
+ activeItemType,
104
+ selectItem,
105
+ clearSelection,
106
+ setFilter,
107
+ toggleExpand,
108
+ copyItems,
109
+ addSession,
110
+ addLesson,
111
+ deleteSession,
112
+ deleteLesson,
113
+ deleteSelected,
114
+ duplicateSelected,
115
+ showConfirm,
116
+ };
117
+ });
118
+
119
+ const handleKeyDown = useCallback(
120
+ (e: KeyboardEvent) => {
121
+ const target = document.activeElement;
122
+ const ctrl = e.ctrlKey || e.metaKey;
123
+
124
+ // ── Ctrl/Cmd + ? always work (help, search) ──────────────────────────
125
+ if (ctrl && e.key === '/') {
126
+ e.preventDefault();
127
+ onToggleHelp();
128
+ return;
129
+ }
130
+
131
+ if (ctrl && e.key.toLowerCase() === 'f') {
132
+ e.preventDefault();
133
+ searchRef.current?.focus();
134
+ return;
135
+ }
136
+
137
+ // ── Ctrl/Cmd + S — save active detail panel form ──────────────────────
138
+ if (ctrl && e.key.toLowerCase() === 's') {
139
+ e.preventDefault();
140
+ if (detailPanelRef.current) {
141
+ const submitBtn =
142
+ detailPanelRef.current.querySelector<HTMLButtonElement>(
143
+ 'button[type="submit"]'
144
+ );
145
+ submitBtn?.click();
146
+ }
147
+ return;
148
+ }
149
+
150
+ // ── Suppress all other shortcuts when editing ─────────────────────────
151
+ if (isEditingTarget(target)) return;
152
+
153
+ const s = stateRef.current;
154
+
155
+ // Build the current visible list (same logic as the tree)
156
+ const { items } = buildVisibleItems(
157
+ s.course,
158
+ s.sessions,
159
+ s.lessons,
160
+ s.expandedIds,
161
+ s.filterQuery
162
+ );
163
+
164
+ const currentIdx = items.findIndex((i) => i.id === s.activeItemId);
165
+ const current = currentIdx !== -1 ? items[currentIdx] : null;
166
+
167
+ // ── Arrow navigation ──────────────────────────────────────────────────
168
+ if (e.key === 'ArrowDown') {
169
+ e.preventDefault();
170
+ const next = items[currentIdx + 1];
171
+ if (next) s.selectItem(next.id, next.type, {}, items);
172
+ return;
173
+ }
174
+
175
+ if (e.key === 'ArrowUp') {
176
+ e.preventDefault();
177
+ const prev = items[currentIdx - 1];
178
+ if (prev) s.selectItem(prev.id, prev.type, {}, items);
179
+ return;
180
+ }
181
+
182
+ if (e.key === 'ArrowRight') {
183
+ e.preventDefault();
184
+ if (current?.type === 'session' && !s.expandedIds.has(current.id)) {
185
+ s.toggleExpand(current.id);
186
+ }
187
+ return;
188
+ }
189
+
190
+ if (e.key === 'ArrowLeft') {
191
+ e.preventDefault();
192
+ if (current?.type === 'session' && s.expandedIds.has(current.id)) {
193
+ s.toggleExpand(current.id);
194
+ } else if (current?.type === 'lesson') {
195
+ // Go to parent session
196
+ const parent = items.find(
197
+ (i) => i.type === 'session' && i.id === current.data.sessionId
198
+ );
199
+ if (parent) s.selectItem(parent.id, 'session', {}, items);
200
+ }
201
+ return;
202
+ }
203
+
204
+ // ── Enter — focus first detail field ─────────────────────────────────
205
+ if (e.key === 'Enter') {
206
+ e.preventDefault();
207
+ if (detailPanelRef.current) {
208
+ const firstInput = detailPanelRef.current.querySelector<HTMLElement>(
209
+ 'input:not([disabled]),textarea:not([disabled])'
210
+ );
211
+ firstInput?.focus();
212
+ }
213
+ return;
214
+ }
215
+
216
+ // ── Escape ────────────────────────────────────────────────────────────
217
+ if (e.key === 'Escape') {
218
+ e.preventDefault();
219
+ if (s.filterQuery) {
220
+ s.setFilter('');
221
+ searchRef.current?.clear();
222
+ } else if (s.selectedIds.size > 1) {
223
+ s.clearSelection();
224
+ } else {
225
+ (document.activeElement as HTMLElement)?.blur();
226
+ }
227
+ return;
228
+ }
229
+
230
+ // ── Delete ────────────────────────────────────────────────────────────
231
+ if (e.key === 'Delete') {
232
+ e.preventDefault();
233
+ const ids = [...s.selectedIds].filter(
234
+ (key) => key !== `course:${s.course.id}`
235
+ );
236
+ if (!ids.length) return;
237
+ const count = ids.length;
238
+ s.showConfirm({
239
+ title: `Excluir ${count > 1 ? count + ' itens' : 'item selecionado'}?`,
240
+ description: 'Esta ação não pode ser desfeita.',
241
+ onConfirm: () => {
242
+ s.deleteSelected();
243
+ toast.success(
244
+ count > 1 ? `${count} itens excluídos` : 'Item excluído'
245
+ );
246
+ },
247
+ });
248
+ return;
249
+ }
250
+
251
+ // ── Ctrl/Cmd + C ─────────────────────────────────────────────────────
252
+ if (ctrl && e.key.toLowerCase() === 'c') {
253
+ e.preventDefault();
254
+ const selectedKeys = [...s.selectedIds].filter(
255
+ (key) => key.startsWith('lesson:') || key.startsWith('session:')
256
+ );
257
+ if (!selectedKeys.length) return;
258
+ const firstKey = selectedKeys[0]!;
259
+ const type = firstKey.startsWith('lesson:') ? 'lesson' : 'session';
260
+ const ids = selectedKeys.map((key) => key.slice(key.indexOf(':') + 1));
261
+ s.copyItems(ids, type);
262
+ toast.success(
263
+ `${ids.length > 1 ? ids.length + ' itens copiados' : 'Item copiado'}`
264
+ );
265
+ return;
266
+ }
267
+
268
+ // ── Ctrl/Cmd + D — duplicate ──────────────────────────────────────────
269
+ if (ctrl && e.key.toLowerCase() === 'd') {
270
+ e.preventDefault();
271
+ if (onDuplicate) {
272
+ onDuplicate();
273
+ } else {
274
+ s.duplicateSelected();
275
+ toast.success('Item duplicado');
276
+ }
277
+ return;
278
+ }
279
+
280
+ // ── Ctrl/Cmd + V — paste ──────────────────────────────────────────────
281
+ if (ctrl && e.key.toLowerCase() === 'v') {
282
+ e.preventDefault();
283
+ if (onPaste) {
284
+ onPaste();
285
+ } else {
286
+ toast('Nada para colar');
287
+ }
288
+ return;
289
+ }
290
+
291
+ // ── Ctrl/Cmd + N — new item ───────────────────────────────────────────
292
+ if (ctrl && e.key.toLowerCase() === 'n') {
293
+ e.preventDefault();
294
+ if (s.activeItemType === 'course') {
295
+ s.addSession();
296
+ toast.success('Nova sessão criada');
297
+ } else if (s.activeItemType === 'session' && s.activeItemId) {
298
+ s.addLesson(s.activeItemId);
299
+ toast.success('Nova aula criada');
300
+ } else if (s.activeItemType === 'lesson') {
301
+ // Find parent session and add there
302
+ const lesson = s.lessons.find((l) => l.id === s.activeItemId);
303
+ if (lesson) {
304
+ s.addLesson(lesson.sessionId);
305
+ toast.success('Nova aula criada na mesma sessão');
306
+ }
307
+ }
308
+ return;
309
+ }
310
+ },
311
+ [onToggleHelp, searchRef, detailPanelRef, onDuplicate, onPaste]
312
+ );
313
+
314
+ useEffect(() => {
315
+ window.addEventListener('keydown', handleKeyDown);
316
+ return () => window.removeEventListener('keydown', handleKeyDown);
317
+ }, [handleKeyDown]);
318
+ }
@@ -0,0 +1,97 @@
1
+ 'use client';
2
+
3
+ import { create } from 'zustand';
4
+
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ // Types
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+
9
+ export interface TreeDisplaySettings {
10
+ /** Show a colored dot indicating lesson production status */
11
+ showStatusDot: boolean;
12
+ /** Show a visibility icon (público / privado / restrito) */
13
+ showVisibility: boolean;
14
+ /** Show the lesson code inline */
15
+ showCode: boolean;
16
+ /** Show a video indicator icon when a video lesson has a video URL set */
17
+ showVideoIndicator: boolean;
18
+ /** Show a paperclip icon when the lesson has downloadable resources */
19
+ showResourcesIndicator: boolean;
20
+ /** Show a transcription icon when a video lesson has transcription text */
21
+ showTranscriptionIndicator: boolean;
22
+ }
23
+
24
+ interface TreeDisplaySettingsStore extends TreeDisplaySettings {
25
+ update: (partial: Partial<TreeDisplaySettings>) => void;
26
+ }
27
+
28
+ // ─────────────────────────────────────────────────────────────────────────────
29
+ // Constants
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+
32
+ const LS_KEY = 'lms:tree-display-settings';
33
+
34
+ const DEFAULTS: TreeDisplaySettings = {
35
+ showStatusDot: true,
36
+ showVisibility: false,
37
+ showCode: false,
38
+ showVideoIndicator: false,
39
+ showResourcesIndicator: false,
40
+ showTranscriptionIndicator: false,
41
+ };
42
+
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+ // Store
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+
47
+ export const useTreeDisplaySettings = create<TreeDisplaySettingsStore>(
48
+ (set, get) => ({
49
+ ...DEFAULTS,
50
+
51
+ update: (partial) => {
52
+ set(partial);
53
+ if (typeof window !== 'undefined') {
54
+ try {
55
+ const s = get();
56
+ const toSave: TreeDisplaySettings = {
57
+ showStatusDot: s.showStatusDot,
58
+ showVisibility: s.showVisibility,
59
+ showCode: s.showCode,
60
+ showVideoIndicator: s.showVideoIndicator,
61
+ showResourcesIndicator: s.showResourcesIndicator,
62
+ showTranscriptionIndicator: s.showTranscriptionIndicator,
63
+ ...partial,
64
+ };
65
+ localStorage.setItem(LS_KEY, JSON.stringify(toSave));
66
+ } catch {
67
+ // ignore
68
+ }
69
+ }
70
+ },
71
+ })
72
+ );
73
+
74
+ // ── Hydrate from localStorage (client-only, runs once at module load) ─────────
75
+ if (typeof window !== 'undefined') {
76
+ try {
77
+ const saved = JSON.parse(
78
+ localStorage.getItem(LS_KEY) ?? '{}'
79
+ ) as Partial<TreeDisplaySettings>;
80
+ if (Object.keys(saved).length > 0) {
81
+ useTreeDisplaySettings.setState({
82
+ showStatusDot: saved.showStatusDot ?? DEFAULTS.showStatusDot,
83
+ showVisibility: saved.showVisibility ?? DEFAULTS.showVisibility,
84
+ showCode: saved.showCode ?? DEFAULTS.showCode,
85
+ showVideoIndicator:
86
+ saved.showVideoIndicator ?? DEFAULTS.showVideoIndicator,
87
+ showResourcesIndicator:
88
+ saved.showResourcesIndicator ?? DEFAULTS.showResourcesIndicator,
89
+ showTranscriptionIndicator:
90
+ saved.showTranscriptionIndicator ??
91
+ DEFAULTS.showTranscriptionIndicator,
92
+ });
93
+ }
94
+ } catch {
95
+ // ignore — use defaults
96
+ }
97
+ }