@hed-hog/lms 0.0.328 → 0.0.330

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 (52) hide show
  1. package/dist/instructor/instructor.service.d.ts.map +1 -1
  2. package/dist/instructor/instructor.service.js +22 -16
  3. package/dist/instructor/instructor.service.js.map +1 -1
  4. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +18 -8
  5. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +7 -5
  6. package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +5 -9
  7. package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +5 -9
  8. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +15 -14
  9. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +66 -29
  10. package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +4 -2
  11. package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -34
  12. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +10 -10
  13. package/hedhog/frontend/app/classes/page.tsx.ejs +23 -15
  14. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +5 -3
  15. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +5 -3
  16. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +9 -7
  17. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +3 -1
  18. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +4 -2
  19. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +24 -23
  20. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +21 -19
  21. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +7 -5
  22. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +18 -16
  23. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +13 -11
  24. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +5 -3
  25. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +14 -9
  26. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +42 -25
  27. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +3 -1
  28. package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +10 -8
  29. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +22 -20
  30. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +3 -3
  31. package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +21 -19
  32. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +34 -36
  33. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +3 -1
  34. package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +7 -5
  35. package/hedhog/frontend/app/enterprise/page.tsx.ejs +106 -54
  36. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +79 -59
  37. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +92 -26
  38. package/hedhog/frontend/app/instructors/page.tsx.ejs +4 -2
  39. package/hedhog/frontend/messages/en.json +619 -13
  40. package/hedhog/frontend/messages/pt.json +619 -13
  41. package/package.json +7 -7
  42. package/src/instructor/instructor.service.ts +22 -19
  43. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +0 -591
  44. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +0 -109
  45. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +0 -60
  46. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +0 -134
  47. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +0 -113
  48. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +0 -314
  49. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +0 -174
  50. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +0 -185
  51. package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +0 -277
  52. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +0 -207
@@ -1,109 +0,0 @@
1
- import { RichTextEditor } from '@/components/rich-text-editor';
2
- import {
3
- FormControl,
4
- FormField,
5
- FormItem,
6
- FormLabel,
7
- FormMessage,
8
- } from '@/components/ui/form';
9
- import { Input } from '@/components/ui/input';
10
- import { Hash, NotebookPen } from 'lucide-react';
11
- import type { UseFormReturn } from 'react-hook-form';
12
-
13
- import { CourseSectionCard } from './CourseSectionCard';
14
- import type { CourseEditFormValues, TranslationFn } from './course-edit-types';
15
-
16
- type CourseMainInfoCardProps = {
17
- form: UseFormReturn<CourseEditFormValues>;
18
- t: TranslationFn;
19
- };
20
-
21
- export function CourseMainInfoCard({ form, t }: CourseMainInfoCardProps) {
22
- return (
23
- <CourseSectionCard
24
- title={t('form.sections.basicInfo')}
25
- description="Os identificadores e a comunicação principal do curso ficam aqui."
26
- icon={NotebookPen}
27
- >
28
- <div className="grid gap-3 md:grid-cols-2">
29
- <FormField
30
- control={form.control}
31
- name="slug"
32
- render={({ field }) => (
33
- <FormItem>
34
- <FormLabel>{t('form.fields.slug.label')}</FormLabel>
35
- <FormControl>
36
- <div className="relative">
37
- <Hash className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
38
- <Input
39
- {...field}
40
- className="pl-9 font-mono"
41
- placeholder={t('form.fields.slug.placeholder')}
42
- onChange={(event) =>
43
- field.onChange(
44
- String(event.target.value || '').toLowerCase()
45
- )
46
- }
47
- />
48
- </div>
49
- </FormControl>
50
- <FormMessage />
51
- </FormItem>
52
- )}
53
- />
54
-
55
- <FormField
56
- control={form.control}
57
- name="nomeInterno"
58
- render={({ field }) => (
59
- <FormItem>
60
- <FormLabel>{t('form.fields.internalName.label')}</FormLabel>
61
- <FormControl>
62
- <Input
63
- {...field}
64
- placeholder={t('form.fields.internalName.placeholder')}
65
- />
66
- </FormControl>
67
- <FormMessage />
68
- </FormItem>
69
- )}
70
- />
71
-
72
- <FormField
73
- control={form.control}
74
- name="tituloComercial"
75
- render={({ field }) => (
76
- <FormItem className="md:col-span-2">
77
- <FormLabel>{t('form.fields.title.label')}</FormLabel>
78
- <FormControl>
79
- <Input
80
- {...field}
81
- placeholder={t('form.fields.title.placeholder')}
82
- />
83
- </FormControl>
84
- <FormMessage />
85
- </FormItem>
86
- )}
87
- />
88
-
89
- <FormField
90
- control={form.control}
91
- name="descricaoPublica"
92
- render={({ field }) => (
93
- <FormItem className="md:col-span-2">
94
- <FormLabel>{t('form.fields.description.label')}</FormLabel>
95
- <FormControl>
96
- <RichTextEditor
97
- value={field.value}
98
- onChange={field.onChange}
99
- className="w-full max-w-full"
100
- />
101
- </FormControl>
102
- <FormMessage />
103
- </FormItem>
104
- )}
105
- />
106
- </div>
107
- </CourseSectionCard>
108
- );
109
- }
@@ -1,60 +0,0 @@
1
- import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
2
- import { CalendarDays, Percent, Users, Video } from 'lucide-react';
3
-
4
- interface CourseSummaryCardProps {
5
- course: {
6
- totalAlunos?: number;
7
- conclusaoMedia?: number;
8
- totalAulas?: number;
9
- totalSessoes?: number;
10
- } | null;
11
- }
12
-
13
- export function CourseSummaryCard({ course }: CourseSummaryCardProps) {
14
- return (
15
- <KpiCardsGrid
16
- items={[
17
- {
18
- key: 'students',
19
- title: 'Total Alunos',
20
- value: (course?.totalAlunos ?? 0).toLocaleString('pt-BR'),
21
- icon: Users,
22
- layout: 'compact',
23
- accentClassName: 'from-slate-900/20 via-slate-700/10 to-transparent',
24
- iconContainerClassName: 'bg-slate-900 text-white',
25
- },
26
- {
27
- key: 'completion',
28
- title: 'Conclusao Media',
29
- value: `${course?.conclusaoMedia ?? 0}%`,
30
- icon: Percent,
31
- layout: 'compact',
32
- accentClassName:
33
- 'from-emerald-500/25 via-green-500/15 to-transparent',
34
- iconContainerClassName: 'bg-emerald-100 text-emerald-700',
35
- },
36
- {
37
- key: 'lessons',
38
- title: 'Total de Aulas',
39
- value: String(course?.totalAulas ?? 0),
40
- icon: Video,
41
- layout: 'compact',
42
- accentClassName: 'from-sky-500/25 via-blue-500/15 to-transparent',
43
- iconContainerClassName: 'bg-sky-100 text-sky-700',
44
- },
45
- {
46
- key: 'sessions',
47
- title: 'Sessoes',
48
- value: String(course?.totalSessoes ?? 0),
49
- icon: CalendarDays,
50
- layout: 'compact',
51
- accentClassName:
52
- 'from-violet-500/25 via-indigo-500/15 to-transparent',
53
- iconContainerClassName: 'bg-violet-100 text-violet-700',
54
- },
55
- ]}
56
- columns={4}
57
- className="mb-3"
58
- />
59
- );
60
- }
@@ -1,134 +0,0 @@
1
- 'use client';
2
-
3
- import { useVirtualizer } from '@tanstack/react-virtual';
4
- import { SearchX } from 'lucide-react';
5
- import { useCallback, useMemo, useRef } from 'react';
6
-
7
- import { useStructureStore } from './store';
8
- import { buildLessonCountMap, buildVisibleItems } from './tree-helpers';
9
- import { TreeRow } from './tree-row';
10
- import type { FlatItem } from './types';
11
-
12
- // Row height estimates per node type (px)
13
- const ROW_HEIGHT: Record<FlatItem['type'], number> = {
14
- course: 34,
15
- session: 32,
16
- lesson: 28,
17
- };
18
-
19
- export function CourseTree() {
20
- const course = useStructureStore((s) => s.course);
21
- const sessions = useStructureStore((s) => s.sessions);
22
- const lessons = useStructureStore((s) => s.lessons);
23
- const expandedIds = useStructureStore((s) => s.expandedIds);
24
- const filterQuery = useStructureStore((s) => s.filterQuery);
25
- const activeItemId = useStructureStore((s) => s.activeItemId);
26
- const activeItemType = useStructureStore((s) => s.activeItemType);
27
- const selectedIds = useStructureStore((s) => s.selectedIds);
28
-
29
- const scrollRef = useRef<HTMLDivElement>(null);
30
-
31
- // ── Derived state ───────────────────────────────────────────────────────────
32
- const { items, matchedIds, expandedBySearch, resultCount } = useMemo(
33
- () =>
34
- buildVisibleItems(course, sessions, lessons, expandedIds, filterQuery),
35
- [course, sessions, lessons, expandedIds, filterQuery]
36
- );
37
-
38
- const lessonCountMap = useMemo(() => buildLessonCountMap(lessons), [lessons]);
39
-
40
- const estimateSize = useCallback(
41
- (index: number) => ROW_HEIGHT[items[index]?.type ?? 'lesson'] ?? 32,
42
- [items]
43
- );
44
-
45
- // ── Virtualizer ─────────────────────────────────────────────────────────────
46
- // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Virtual returns non-memoizable functions; this is expected and safe here
47
- const virtualizer = useVirtualizer({
48
- count: items.length,
49
- getScrollElement: () => scrollRef.current,
50
- estimateSize,
51
- overscan: 8,
52
- });
53
-
54
- const isSearchActive = filterQuery.trim().length > 0;
55
- const isEmpty = items.length === 0;
56
-
57
- return (
58
- <div className="flex flex-col h-full min-h-0">
59
- {/* Result count bar */}
60
- {isSearchActive && !isEmpty && (
61
- <div className="px-3 py-1 shrink-0 border-b">
62
- <span className="text-[0.65rem] text-muted-foreground">
63
- {resultCount === 1 ? '1 resultado' : `${resultCount} resultados`}
64
- </span>
65
- </div>
66
- )}
67
-
68
- {/* Empty state */}
69
- {isEmpty && (
70
- <div className="flex flex-col items-center justify-center gap-2 py-12 px-4 text-center">
71
- <SearchX className="size-8 text-muted-foreground/40" />
72
- <p className="text-sm text-muted-foreground">
73
- {isSearchActive
74
- ? `Nenhum resultado para "${filterQuery.trim()}"`
75
- : 'Nenhum item no curso.'}
76
- </p>
77
- </div>
78
- )}
79
-
80
- {/* Virtualised tree */}
81
- {!isEmpty && (
82
- <div
83
- ref={scrollRef}
84
- className="overflow-y-auto flex-1 min-h-0"
85
- style={{ contain: 'strict' }}
86
- >
87
- <div
88
- style={{
89
- height: virtualizer.getTotalSize(),
90
- width: '100%',
91
- position: 'relative',
92
- }}
93
- >
94
- {virtualizer.getVirtualItems().map((virtualRow) => {
95
- const item = items[virtualRow.index];
96
- if (!item) return null;
97
-
98
- return (
99
- <div
100
- key={virtualRow.key}
101
- data-index={virtualRow.index}
102
- style={{
103
- position: 'absolute',
104
- top: 0,
105
- left: 0,
106
- width: '100%',
107
- height: `${virtualRow.size}px`,
108
- transform: `translateY(${virtualRow.start}px)`,
109
- padding: '1px 4px',
110
- }}
111
- >
112
- <TreeRow
113
- item={item}
114
- isActive={
115
- activeItemId === item.id && activeItemType === item.type
116
- }
117
- isSelected={selectedIds.has(`${item.type}:${item.id}`)}
118
- query={filterQuery}
119
- isMatched={matchedIds.has(item.id)}
120
- isEffectivelyExpanded={
121
- expandedIds.has(item.id) || expandedBySearch.has(item.id)
122
- }
123
- lessonCountMap={lessonCountMap}
124
- visibleItems={items}
125
- />
126
- </div>
127
- );
128
- })}
129
- </div>
130
- </div>
131
- )}
132
- </div>
133
- );
134
- }
@@ -1,113 +0,0 @@
1
- 'use client';
2
-
3
- import { Badge } from '@/components/ui/badge';
4
- import { Separator } from '@/components/ui/separator';
5
- import { BookOpen, Globe, Hash, Tag } from 'lucide-react';
6
- import { useStructureStore } from './store';
7
-
8
- export function DetailCourse() {
9
- const course = useStructureStore((s) => s.course);
10
- const sessions = useStructureStore((s) => s.sessions);
11
- const lessons = useStructureStore((s) => s.lessons);
12
-
13
- const totalMinutes = lessons.reduce((sum, l) => sum + l.duration, 0);
14
- const hours = Math.floor(totalMinutes / 60);
15
- const minutes = totalMinutes % 60;
16
-
17
- return (
18
- <div className="flex flex-col overflow-y-auto h-full">
19
- {/* Header */}
20
- <div className="flex items-center gap-3 px-4 py-4 border-b bg-muted/30 shrink-0">
21
- <div className="flex size-10 items-center justify-center rounded-lg bg-primary/10 shrink-0">
22
- <BookOpen className="size-5 text-primary" />
23
- </div>
24
- <div className="min-w-0 flex-1">
25
- <h2 className="text-base font-semibold truncate">{course.title}</h2>
26
- <p className="text-xs text-muted-foreground">{course.slug}</p>
27
- </div>
28
- <Badge
29
- variant={course.published ? 'default' : 'secondary'}
30
- className="shrink-0"
31
- >
32
- {course.published ? 'Publicado' : 'Rascunho'}
33
- </Badge>
34
- </div>
35
-
36
- <div className="flex flex-col gap-5 p-4">
37
- {/* Stats */}
38
- <div className="grid grid-cols-3 gap-3">
39
- <StatCard label="Sessões" value={sessions.length} />
40
- <StatCard label="Aulas" value={lessons.length} />
41
- <StatCard
42
- label="Duração"
43
- value={hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`}
44
- />
45
- </div>
46
-
47
- <Separator />
48
-
49
- {/* Info */}
50
- <div className="flex flex-col gap-2.5">
51
- <InfoRow
52
- icon={<Hash className="size-3.5" />}
53
- label="Slug"
54
- value={course.slug}
55
- />
56
- <InfoRow
57
- icon={<Tag className="size-3.5" />}
58
- label="Slug"
59
- value={course.slug}
60
- />
61
- <InfoRow
62
- icon={<Globe className="size-3.5" />}
63
- label="Status"
64
- value={course.published ? 'Publicado' : 'Rascunho'}
65
- />
66
- </div>
67
-
68
- {course.description && (
69
- <>
70
- <Separator />
71
- <div>
72
- <p className="text-xs font-medium text-muted-foreground mb-1.5">
73
- Descrição
74
- </p>
75
- <p className="text-sm leading-relaxed text-foreground/90">
76
- {course.description}
77
- </p>
78
- </div>
79
- </>
80
- )}
81
- </div>
82
- </div>
83
- );
84
- }
85
-
86
- function StatCard({ label, value }: { label: string; value: string | number }) {
87
- return (
88
- <div className="flex flex-col items-center rounded-lg border bg-muted/30 py-3 gap-0.5">
89
- <span className="text-lg font-bold tabular-nums">{value}</span>
90
- <span className="text-[0.65rem] text-muted-foreground">{label}</span>
91
- </div>
92
- );
93
- }
94
-
95
- function InfoRow({
96
- icon,
97
- label,
98
- value,
99
- }: {
100
- icon: React.ReactNode;
101
- label: string;
102
- value: string;
103
- }) {
104
- return (
105
- <div className="flex items-center gap-2">
106
- <span className="text-muted-foreground shrink-0">{icon}</span>
107
- <span className="text-xs text-muted-foreground w-14 shrink-0">
108
- {label}
109
- </span>
110
- <span className="text-sm truncate">{value}</span>
111
- </div>
112
- );
113
- }