@hed-hog/lms 0.0.355 → 0.0.358
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.
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
- package/dist/course/course-audio-transcription.service.js +15 -7
- package/dist/course/course-audio-transcription.service.js.map +1 -1
- package/dist/course/course-operations-integration.service.d.ts +31 -0
- package/dist/course/course-operations-integration.service.d.ts.map +1 -1
- package/dist/course/course-operations-integration.service.js +286 -22
- package/dist/course/course-operations-integration.service.js.map +1 -1
- package/dist/course/course-operations.controller.d.ts +10 -0
- package/dist/course/course-operations.controller.d.ts.map +1 -0
- package/dist/course/course-operations.controller.js +67 -0
- package/dist/course/course-operations.controller.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +15 -1
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.service.d.ts +25 -1
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +160 -24
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +4 -1
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +4 -2
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +61 -2
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +3 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +15 -0
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course-structure-session.dto.d.ts +1 -0
- package/dist/course/dto/create-course-structure-session.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-session.dto.js +5 -0
- package/dist/course/dto/create-course-structure-session.dto.js.map +1 -1
- package/dist/course/dto/update-course-operations-config.dto.d.ts +6 -0
- package/dist/course/dto/update-course-operations-config.dto.d.ts.map +1 -0
- package/dist/course/dto/update-course-operations-config.dto.js +33 -0
- package/dist/course/dto/update-course-operations-config.dto.js.map +1 -0
- package/dist/course/lms-operations-task.subscriber.d.ts +13 -0
- package/dist/course/lms-operations-task.subscriber.d.ts.map +1 -0
- package/dist/course/lms-operations-task.subscriber.js +57 -0
- package/dist/course/lms-operations-task.subscriber.js.map +1 -0
- package/dist/enterprise/enterprise.service.js +1 -1
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/enterprise/training/training-student.controller.d.ts +0 -95
- package/dist/enterprise/training/training-student.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.controller.js +1 -34
- package/dist/enterprise/training/training-student.controller.js.map +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +63 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +320 -4
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +12 -3
- package/dist/instructor/instructor.service.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +2 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/platforma.controller.d.ts +287 -0
- package/dist/platforma/platforma.controller.d.ts.map +1 -0
- package/dist/platforma/platforma.controller.js +147 -0
- package/dist/platforma/platforma.controller.js.map +1 -0
- package/hedhog/data/route.yaml +102 -9
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +10 -13
- package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +26 -4
- package/hedhog/frontend/app/_lib/editor/types.ts.ejs +6 -0
- package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +447 -31
- package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +59 -47
- package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +201 -35
- package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +36 -5
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +17 -6
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +382 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +45 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +177 -67
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +25 -60
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +11 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +22 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +79 -64
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +31 -14
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +4 -4
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +39 -27
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +24 -2
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +41 -6
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +2 -2
- package/hedhog/frontend/app/courses/page.tsx.ejs +80 -103
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +18 -6
- package/hedhog/frontend/app/exams/[id]/page.tsx.ejs +5 -4
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +16 -10
- package/hedhog/frontend/app/paths/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/training/page.tsx.ejs +1 -1
- package/hedhog/frontend/messages/en.json +7 -2
- package/hedhog/frontend/messages/pt.json +7 -2
- package/hedhog/table/course_lesson.yaml +2 -2
- package/hedhog/table/course_module.yaml +3 -0
- package/package.json +8 -8
- package/src/course/course-audio-transcription.service.ts +21 -8
- package/src/course/course-operations-integration.service.ts +460 -22
- package/src/course/course-operations.controller.ts +45 -0
- package/src/course/course-structure.service.ts +209 -4
- package/src/course/course.module.ts +4 -1
- package/src/course/course.service.ts +67 -1
- package/src/course/dto/create-course-structure-lesson.dto.ts +17 -0
- package/src/course/dto/create-course-structure-session.dto.ts +13 -1
- package/src/course/dto/update-course-operations-config.dto.ts +16 -0
- package/src/course/lms-operations-task.subscriber.ts +44 -0
- package/src/enterprise/enterprise.service.ts +1 -1
- package/src/enterprise/training/training-student.controller.ts +3 -27
- package/src/enterprise/training/training-student.service.ts +350 -2
- package/src/instructor/instructor.service.ts +12 -3
- package/src/lms.module.ts +2 -0
- package/src/platforma/platforma.controller.ts +92 -0
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
GripVertical,
|
|
9
9
|
ImageIcon,
|
|
10
10
|
Minus,
|
|
11
|
+
QrCode,
|
|
11
12
|
RectangleHorizontal,
|
|
12
13
|
Type,
|
|
13
14
|
UploadCloud,
|
|
@@ -60,8 +61,8 @@ export default function LeftPanel() {
|
|
|
60
61
|
);
|
|
61
62
|
|
|
62
63
|
return (
|
|
63
|
-
<aside className="flex
|
|
64
|
-
<Tabs defaultValue="elements" className="flex flex-1 flex-col">
|
|
64
|
+
<aside className="flex h-full min-h-0 w-full flex-col border-r border-border bg-background">
|
|
65
|
+
<Tabs defaultValue="elements" className="flex min-h-0 flex-1 flex-col">
|
|
65
66
|
<TabsList className="mx-3 mt-3 w-auto">
|
|
66
67
|
<TabsTrigger value="elements" className="flex-1">
|
|
67
68
|
{t('leftPanel.tabs.elements')}
|
|
@@ -72,18 +73,21 @@ export default function LeftPanel() {
|
|
|
72
73
|
</TabsList>
|
|
73
74
|
|
|
74
75
|
{/* ── ELEMENTS ── */}
|
|
75
|
-
<TabsContent
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
<TabsContent
|
|
77
|
+
value="elements"
|
|
78
|
+
className="min-h-0 flex-1 overflow-hidden"
|
|
79
|
+
>
|
|
80
|
+
<ScrollArea className="h-full min-h-0">
|
|
81
|
+
<div className="flex flex-col gap-1.5 p-2.5">
|
|
78
82
|
{/* fields */}
|
|
79
83
|
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
80
84
|
{t('leftPanel.sections.fields')}
|
|
81
85
|
</p>
|
|
82
|
-
<div className="grid grid-cols-
|
|
83
|
-
{FIELD_KEYS.map((k) => (
|
|
86
|
+
<div className="grid grid-cols-1 gap-1.5">
|
|
87
|
+
{FIELD_KEYS.filter((k) => k !== 'qrCode').map((k) => (
|
|
84
88
|
<ElementCard
|
|
85
89
|
key={k}
|
|
86
|
-
icon={<FileText className="size-
|
|
90
|
+
icon={<FileText className="size-3.5" />}
|
|
87
91
|
label={FIELD_LABELS[k as FieldKey]}
|
|
88
92
|
onClick={() => add('field', { key: k })}
|
|
89
93
|
dragPayload={{ type: 'field', key: k }}
|
|
@@ -92,29 +96,29 @@ export default function LeftPanel() {
|
|
|
92
96
|
</div>
|
|
93
97
|
|
|
94
98
|
{/* static text */}
|
|
95
|
-
<p className="mt-
|
|
99
|
+
<p className="mt-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
96
100
|
{t('leftPanel.sections.text')}
|
|
97
101
|
</p>
|
|
98
102
|
<ElementCard
|
|
99
|
-
icon={<Type className="size-
|
|
103
|
+
icon={<Type className="size-3.5" />}
|
|
100
104
|
label={t('leftPanel.elements.staticText')}
|
|
101
105
|
onClick={() => add('staticText')}
|
|
102
106
|
dragPayload={{ type: 'staticText' }}
|
|
103
107
|
/>
|
|
104
108
|
|
|
105
109
|
{/* shapes */}
|
|
106
|
-
<p className="mt-
|
|
110
|
+
<p className="mt-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
107
111
|
{t('leftPanel.sections.shapes')}
|
|
108
112
|
</p>
|
|
109
|
-
<div className="grid grid-cols-2 gap-
|
|
113
|
+
<div className="grid grid-cols-2 gap-1.5">
|
|
110
114
|
<ElementCard
|
|
111
|
-
icon={<RectangleHorizontal className="size-
|
|
115
|
+
icon={<RectangleHorizontal className="size-3.5" />}
|
|
112
116
|
label={t('leftPanel.elements.rectangle')}
|
|
113
117
|
onClick={() => add('shape', { shape: 'rect' })}
|
|
114
118
|
dragPayload={{ type: 'shape', shape: 'rect' }}
|
|
115
119
|
/>
|
|
116
120
|
<ElementCard
|
|
117
|
-
icon={<Minus className="size-
|
|
121
|
+
icon={<Minus className="size-3.5" />}
|
|
118
122
|
label={t('leftPanel.elements.line')}
|
|
119
123
|
onClick={() => add('shape', { shape: 'line' })}
|
|
120
124
|
dragPayload={{ type: 'shape', shape: 'line' }}
|
|
@@ -122,25 +126,31 @@ export default function LeftPanel() {
|
|
|
122
126
|
</div>
|
|
123
127
|
|
|
124
128
|
{/* image */}
|
|
125
|
-
<p className="mt-
|
|
129
|
+
<p className="mt-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
126
130
|
{t('leftPanel.sections.image')}
|
|
127
131
|
</p>
|
|
128
|
-
<div className="grid grid-cols-1 gap-
|
|
132
|
+
<div className="grid grid-cols-1 gap-1.5">
|
|
129
133
|
<ElementCard
|
|
130
|
-
icon={<ImageIcon className="size-
|
|
134
|
+
icon={<ImageIcon className="size-3.5" />}
|
|
131
135
|
label="Logo do Curso"
|
|
132
136
|
onClick={() => add('image', { key: 'courseLogo' })}
|
|
133
137
|
dragPayload={{ type: 'image', key: 'courseLogo' }}
|
|
134
138
|
/>
|
|
135
139
|
<ElementCard
|
|
136
|
-
icon={<ImageIcon className="size-
|
|
140
|
+
icon={<ImageIcon className="size-3.5" />}
|
|
137
141
|
label="Banner do Curso"
|
|
138
142
|
onClick={() => add('image', { key: 'courseBanner' })}
|
|
139
143
|
dragPayload={{ type: 'image', key: 'courseBanner' }}
|
|
140
144
|
/>
|
|
145
|
+
<ElementCard
|
|
146
|
+
icon={<QrCode className="size-3.5" />}
|
|
147
|
+
label={FIELD_LABELS.qrCode}
|
|
148
|
+
onClick={() => add('field', { key: 'qrCode' })}
|
|
149
|
+
dragPayload={{ type: 'field', key: 'qrCode' }}
|
|
150
|
+
/>
|
|
141
151
|
</div>
|
|
142
152
|
<ElementCard
|
|
143
|
-
icon={<ImageIcon className="size-
|
|
153
|
+
icon={<ImageIcon className="size-3.5" />}
|
|
144
154
|
label={t('leftPanel.elements.image')}
|
|
145
155
|
onClick={() => add('image')}
|
|
146
156
|
dragPayload={{ type: 'image' }}
|
|
@@ -150,32 +160,34 @@ export default function LeftPanel() {
|
|
|
150
160
|
</TabsContent>
|
|
151
161
|
|
|
152
162
|
{/* ── ASSETS ── */}
|
|
153
|
-
<TabsContent value="assets" className="flex-1 overflow-hidden">
|
|
154
|
-
<
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
163
|
+
<TabsContent value="assets" className="min-h-0 flex-1 overflow-hidden">
|
|
164
|
+
<ScrollArea className="h-full min-h-0">
|
|
165
|
+
<div className="flex flex-col gap-4 p-3">
|
|
166
|
+
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
167
|
+
{t('leftPanel.background.title')}
|
|
168
|
+
</p>
|
|
169
|
+
<Button
|
|
170
|
+
variant="outline"
|
|
171
|
+
className="h-24 w-full flex-col gap-2"
|
|
172
|
+
onClick={() => fileInputRef.current?.click()}
|
|
173
|
+
>
|
|
174
|
+
<UploadCloud className="size-6 text-muted-foreground" />
|
|
175
|
+
<span className="text-xs text-muted-foreground">
|
|
176
|
+
{t('leftPanel.background.upload')}
|
|
177
|
+
</span>
|
|
178
|
+
</Button>
|
|
179
|
+
<input
|
|
180
|
+
ref={fileInputRef}
|
|
181
|
+
type="file"
|
|
182
|
+
accept="image/*"
|
|
183
|
+
className="hidden"
|
|
184
|
+
onChange={handleBgUpload}
|
|
185
|
+
/>
|
|
186
|
+
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
|
187
|
+
{t('leftPanel.background.description')}
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
</ScrollArea>
|
|
179
191
|
</TabsContent>
|
|
180
192
|
</Tabs>
|
|
181
193
|
</aside>
|
|
@@ -205,9 +217,9 @@ function ElementCard({
|
|
|
205
217
|
draggable
|
|
206
218
|
onDragStart={handleDragStart}
|
|
207
219
|
onClick={onClick}
|
|
208
|
-
className="group flex items-center gap-
|
|
220
|
+
className="group flex h-8 items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1 text-left text-[11px] font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground active:cursor-grabbing"
|
|
209
221
|
>
|
|
210
|
-
<GripVertical className="size-
|
|
222
|
+
<GripVertical className="size-2.5 shrink-0 text-muted-foreground/50 opacity-0 transition-opacity group-hover:opacity-100" />
|
|
211
223
|
{icon}
|
|
212
224
|
<span className="truncate">{label}</span>
|
|
213
225
|
</button>
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
import { Slider } from '@/components/ui/slider';
|
|
14
14
|
import { Switch } from '@/components/ui/switch';
|
|
15
15
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
16
|
-
import { Textarea } from '@/components/ui/textarea';
|
|
17
16
|
import { useApp } from '@hed-hog/next-app-provider';
|
|
18
17
|
import {
|
|
19
18
|
ChevronDown,
|
|
@@ -28,7 +27,7 @@ import {
|
|
|
28
27
|
Unlock,
|
|
29
28
|
} from 'lucide-react';
|
|
30
29
|
import { useTranslations } from 'next-intl';
|
|
31
|
-
import { useCallback, useRef, useState } from 'react';
|
|
30
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
32
31
|
import { toast } from 'sonner';
|
|
33
32
|
import { getCanvasAPI } from '../../_lib/editor/canvasInstance';
|
|
34
33
|
import { fromPctH, fromPctW } from '../../_lib/editor/pctHelpers';
|
|
@@ -39,10 +38,10 @@ import { useTemplateStore } from '../../_lib/store/useTemplateStore';
|
|
|
39
38
|
export default function RightPanel() {
|
|
40
39
|
const t = useTranslations('lms.CertificateTemplateEditor');
|
|
41
40
|
return (
|
|
42
|
-
<aside className="flex
|
|
41
|
+
<aside className="flex h-full min-h-0 min-w-0 w-full flex-col overflow-hidden border-l border-border bg-background">
|
|
43
42
|
<Tabs
|
|
44
43
|
defaultValue="props"
|
|
45
|
-
className="flex flex-1 flex-col overflow-hidden"
|
|
44
|
+
className="flex min-h-0 flex-1 flex-col overflow-hidden"
|
|
46
45
|
>
|
|
47
46
|
<TabsList className="mx-3 mt-3 w-auto">
|
|
48
47
|
<TabsTrigger value="props" className="flex-1">
|
|
@@ -56,20 +55,20 @@ export default function RightPanel() {
|
|
|
56
55
|
</TabsTrigger>
|
|
57
56
|
</TabsList>
|
|
58
57
|
|
|
59
|
-
<TabsContent value="props" className="flex-1 overflow-hidden">
|
|
60
|
-
<ScrollArea className="h-full">
|
|
58
|
+
<TabsContent value="props" className="min-w-0 flex-1 overflow-hidden">
|
|
59
|
+
<ScrollArea className="h-full min-w-0">
|
|
61
60
|
<PropertiesInspector />
|
|
62
61
|
</ScrollArea>
|
|
63
62
|
</TabsContent>
|
|
64
63
|
|
|
65
|
-
<TabsContent value="layers" className="flex-1 overflow-hidden">
|
|
66
|
-
<ScrollArea className="h-full">
|
|
64
|
+
<TabsContent value="layers" className="min-w-0 flex-1 overflow-hidden">
|
|
65
|
+
<ScrollArea className="h-full min-w-0">
|
|
67
66
|
<LayersList />
|
|
68
67
|
</ScrollArea>
|
|
69
68
|
</TabsContent>
|
|
70
69
|
|
|
71
|
-
<TabsContent value="data" className="flex-1 overflow-hidden">
|
|
72
|
-
<ScrollArea className="h-full">
|
|
70
|
+
<TabsContent value="data" className="min-w-0 flex-1 overflow-hidden">
|
|
71
|
+
<ScrollArea className="h-full min-w-0">
|
|
73
72
|
<DataView />
|
|
74
73
|
</ScrollArea>
|
|
75
74
|
</TabsContent>
|
|
@@ -105,7 +104,9 @@ function PropertiesInspector() {
|
|
|
105
104
|
);
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
const
|
|
107
|
+
const isQrPlaceholder = obj.type === 'field' && obj.key === 'qrCode';
|
|
108
|
+
const isText =
|
|
109
|
+
(obj.type === 'field' || obj.type === 'staticText') && !isQrPlaceholder;
|
|
109
110
|
const isShape = obj.type === 'shape';
|
|
110
111
|
const isImage = obj.type === 'image';
|
|
111
112
|
|
|
@@ -498,6 +499,8 @@ function ImageProperties({
|
|
|
498
499
|
const isCoursePlaceholder =
|
|
499
500
|
obj.key === 'courseLogo' || obj.key === 'courseBanner';
|
|
500
501
|
const [isUploading, setIsUploading] = useState(false);
|
|
502
|
+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
503
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
501
504
|
|
|
502
505
|
const currentFileId =
|
|
503
506
|
typeof obj.image?.file_id === 'number' && Number.isFinite(obj.image.file_id)
|
|
@@ -509,7 +512,50 @@ function ImageProperties({
|
|
|
509
512
|
obj.image?.resizeMode === 'center'
|
|
510
513
|
? obj.image.resizeMode
|
|
511
514
|
: 'contain';
|
|
512
|
-
|
|
515
|
+
|
|
516
|
+
useEffect(() => {
|
|
517
|
+
if (!currentFileId) {
|
|
518
|
+
setPreviewUrl(null);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
let canceled = false;
|
|
523
|
+
(async () => {
|
|
524
|
+
try {
|
|
525
|
+
const openResponse = await request<{ url?: string }>({
|
|
526
|
+
url: `/file/open/${currentFileId}`,
|
|
527
|
+
method: 'PUT',
|
|
528
|
+
});
|
|
529
|
+
if (canceled) return;
|
|
530
|
+
setPreviewUrl(openResponse?.data?.url || `/file/open/${currentFileId}`);
|
|
531
|
+
} catch {
|
|
532
|
+
if (canceled) return;
|
|
533
|
+
setPreviewUrl(`/file/open/${currentFileId}`);
|
|
534
|
+
}
|
|
535
|
+
})();
|
|
536
|
+
|
|
537
|
+
return () => {
|
|
538
|
+
canceled = true;
|
|
539
|
+
};
|
|
540
|
+
}, [currentFileId, request]);
|
|
541
|
+
|
|
542
|
+
useEffect(() => {
|
|
543
|
+
if (isCoursePlaceholder) return;
|
|
544
|
+
|
|
545
|
+
const handler = (event: Event) => {
|
|
546
|
+
const customEvent = event as CustomEvent<{ objectId?: string }>;
|
|
547
|
+
if (customEvent.detail?.objectId !== obj.id) return;
|
|
548
|
+
fileInputRef.current?.click();
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
window.addEventListener('lms-certificate:auto-open-image-upload', handler);
|
|
552
|
+
return () => {
|
|
553
|
+
window.removeEventListener(
|
|
554
|
+
'lms-certificate:auto-open-image-upload',
|
|
555
|
+
handler
|
|
556
|
+
);
|
|
557
|
+
};
|
|
558
|
+
}, [isCoursePlaceholder, obj.id]);
|
|
513
559
|
|
|
514
560
|
async function handleImageUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
|
515
561
|
const file = e.target.files?.[0];
|
|
@@ -530,9 +576,10 @@ function ImageProperties({
|
|
|
530
576
|
id: number;
|
|
531
577
|
url: string;
|
|
532
578
|
}>({
|
|
533
|
-
url: '/
|
|
579
|
+
url: '/file',
|
|
534
580
|
method: 'POST',
|
|
535
581
|
data: formData,
|
|
582
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
536
583
|
});
|
|
537
584
|
|
|
538
585
|
const newFileId = response?.data?.id;
|
|
@@ -540,10 +587,44 @@ function ImageProperties({
|
|
|
540
587
|
throw new Error('Invalid uploaded file id');
|
|
541
588
|
}
|
|
542
589
|
|
|
590
|
+
const openResponse = await request<{ url?: string }>({
|
|
591
|
+
url: `/file/open/${newFileId}`,
|
|
592
|
+
method: 'PUT',
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const openedUrl = openResponse?.data?.url || `/file/open/${newFileId}`;
|
|
596
|
+
|
|
597
|
+
const imageSize = await new Promise<{ width: number; height: number }>(
|
|
598
|
+
(resolve, reject) => {
|
|
599
|
+
const localUrl = URL.createObjectURL(file);
|
|
600
|
+
const image = new Image();
|
|
601
|
+
image.onload = () => {
|
|
602
|
+
const width = image.naturalWidth || image.width || 1;
|
|
603
|
+
const height = image.naturalHeight || image.height || 1;
|
|
604
|
+
URL.revokeObjectURL(localUrl);
|
|
605
|
+
resolve({ width, height });
|
|
606
|
+
};
|
|
607
|
+
image.onerror = () => {
|
|
608
|
+
URL.revokeObjectURL(localUrl);
|
|
609
|
+
reject(new Error('invalid image dimensions'));
|
|
610
|
+
};
|
|
611
|
+
image.src = localUrl;
|
|
612
|
+
}
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
const currentWidth = Math.max(fromPctW(obj.wPct), 80);
|
|
616
|
+
const proportionalHeight = Math.max(
|
|
617
|
+
Math.round((currentWidth * imageSize.height) / imageSize.width),
|
|
618
|
+
40
|
|
619
|
+
);
|
|
620
|
+
|
|
543
621
|
setProp({
|
|
544
622
|
_tplImageFileId: newFileId,
|
|
545
|
-
_tplImageSrc:
|
|
623
|
+
_tplImageSrc: openedUrl,
|
|
624
|
+
width: currentWidth,
|
|
625
|
+
height: proportionalHeight,
|
|
546
626
|
});
|
|
627
|
+
setPreviewUrl(openedUrl);
|
|
547
628
|
|
|
548
629
|
if (
|
|
549
630
|
typeof currentFileId === 'number' &&
|
|
@@ -586,6 +667,76 @@ function ImageProperties({
|
|
|
586
667
|
}
|
|
587
668
|
}
|
|
588
669
|
|
|
670
|
+
async function getNaturalImageSize(src: string) {
|
|
671
|
+
return await new Promise<{ width: number; height: number }>(
|
|
672
|
+
(resolve, reject) => {
|
|
673
|
+
const image = new Image();
|
|
674
|
+
image.onload = () => {
|
|
675
|
+
resolve({
|
|
676
|
+
width: image.naturalWidth || image.width || 1,
|
|
677
|
+
height: image.naturalHeight || image.height || 1,
|
|
678
|
+
});
|
|
679
|
+
};
|
|
680
|
+
image.onerror = () => reject(new Error('invalid image dimensions'));
|
|
681
|
+
image.src = src;
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function handleResizeModeChange(value: string) {
|
|
687
|
+
if (
|
|
688
|
+
value !== 'contain' &&
|
|
689
|
+
value !== 'cover' &&
|
|
690
|
+
value !== 'stretch' &&
|
|
691
|
+
value !== 'center'
|
|
692
|
+
) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const currentWidth = Math.max(fromPctW(obj.wPct), 1);
|
|
697
|
+
const currentHeight = Math.max(fromPctH(obj.hPct), 1);
|
|
698
|
+
|
|
699
|
+
if (value !== 'center') {
|
|
700
|
+
setProp({
|
|
701
|
+
_tplImageResizeMode: value,
|
|
702
|
+
width: currentWidth,
|
|
703
|
+
height: currentHeight,
|
|
704
|
+
scaleX: 1,
|
|
705
|
+
scaleY: 1,
|
|
706
|
+
});
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (!currentFileId) {
|
|
711
|
+
setProp({ _tplImageResizeMode: 'center' });
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
let sourceUrl = previewUrl;
|
|
717
|
+
if (!sourceUrl) {
|
|
718
|
+
const openResponse = await request<{ url?: string }>({
|
|
719
|
+
url: `/file/open/${currentFileId}`,
|
|
720
|
+
method: 'PUT',
|
|
721
|
+
});
|
|
722
|
+
sourceUrl = openResponse?.data?.url || `/file/open/${currentFileId}`;
|
|
723
|
+
setPreviewUrl(sourceUrl);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const natural = await getNaturalImageSize(sourceUrl);
|
|
727
|
+
setProp({
|
|
728
|
+
_tplImageResizeMode: 'center',
|
|
729
|
+
width: natural.width,
|
|
730
|
+
height: natural.height,
|
|
731
|
+
scaleX: 1,
|
|
732
|
+
scaleY: 1,
|
|
733
|
+
});
|
|
734
|
+
} catch {
|
|
735
|
+
setProp({ _tplImageResizeMode: 'center' });
|
|
736
|
+
toast.error('Nao foi possivel obter o tamanho original da imagem.');
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
589
740
|
return (
|
|
590
741
|
<div className="flex flex-col gap-3">
|
|
591
742
|
<div className="flex flex-col gap-1.5">
|
|
@@ -605,6 +756,7 @@ function ImageProperties({
|
|
|
605
756
|
<div className="flex flex-col gap-1.5">
|
|
606
757
|
<Label className="text-xs">Imagem personalizada</Label>
|
|
607
758
|
<input
|
|
759
|
+
ref={fileInputRef}
|
|
608
760
|
type="file"
|
|
609
761
|
accept="image/*"
|
|
610
762
|
onChange={handleImageUpload}
|
|
@@ -635,10 +787,7 @@ function ImageProperties({
|
|
|
635
787
|
|
|
636
788
|
<div className="flex flex-col gap-1.5">
|
|
637
789
|
<Label className="text-xs">Modo de redimensionamento</Label>
|
|
638
|
-
<Select
|
|
639
|
-
value={resizeMode}
|
|
640
|
-
onValueChange={(value) => setProp({ _tplImageResizeMode: value })}
|
|
641
|
-
>
|
|
790
|
+
<Select value={resizeMode} onValueChange={handleResizeModeChange}>
|
|
642
791
|
<SelectTrigger className="w-full">
|
|
643
792
|
<SelectValue />
|
|
644
793
|
</SelectTrigger>
|
|
@@ -664,15 +813,7 @@ function ImageProperties({
|
|
|
664
813
|
<img
|
|
665
814
|
src={previewUrl}
|
|
666
815
|
alt="Preview da imagem do certificado"
|
|
667
|
-
className=
|
|
668
|
-
resizeMode === 'cover'
|
|
669
|
-
? 'object-cover'
|
|
670
|
-
: resizeMode === 'stretch'
|
|
671
|
-
? 'object-fill'
|
|
672
|
-
: resizeMode === 'center'
|
|
673
|
-
? 'object-none object-center'
|
|
674
|
-
: 'object-contain'
|
|
675
|
-
}`}
|
|
816
|
+
className="h-32 w-full object-contain"
|
|
676
817
|
loading="lazy"
|
|
677
818
|
/>
|
|
678
819
|
</div>
|
|
@@ -956,7 +1097,33 @@ function LayerItem({
|
|
|
956
1097
|
|
|
957
1098
|
function DataView() {
|
|
958
1099
|
const template = useTemplateStore((s) => s.template);
|
|
959
|
-
const json = JSON.stringify(template, null, 2);
|
|
1100
|
+
const json = useMemo(() => JSON.stringify(template, null, 2), [template]);
|
|
1101
|
+
|
|
1102
|
+
const highlightedJson = useMemo(() => {
|
|
1103
|
+
const escaped = json
|
|
1104
|
+
.replace(/&/g, '&')
|
|
1105
|
+
.replace(/</g, '<')
|
|
1106
|
+
.replace(/>/g, '>');
|
|
1107
|
+
|
|
1108
|
+
return escaped.replace(
|
|
1109
|
+
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?)/g,
|
|
1110
|
+
(match) => {
|
|
1111
|
+
let className = 'text-violet-600 dark:text-violet-300';
|
|
1112
|
+
|
|
1113
|
+
if (match.startsWith('"')) {
|
|
1114
|
+
className = match.endsWith(':')
|
|
1115
|
+
? 'text-sky-600 dark:text-sky-300'
|
|
1116
|
+
: 'text-emerald-600 dark:text-emerald-300';
|
|
1117
|
+
} else if (match === 'true' || match === 'false') {
|
|
1118
|
+
className = 'text-amber-600 dark:text-amber-300';
|
|
1119
|
+
} else if (match === 'null') {
|
|
1120
|
+
className = 'text-rose-600 dark:text-rose-300';
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
return `<span class="${className}">${match}</span>`;
|
|
1124
|
+
}
|
|
1125
|
+
);
|
|
1126
|
+
}, [json]);
|
|
960
1127
|
|
|
961
1128
|
const handleCopy = useCallback(() => {
|
|
962
1129
|
navigator.clipboard.writeText(json).then(() => {
|
|
@@ -965,7 +1132,7 @@ function DataView() {
|
|
|
965
1132
|
}, [json]);
|
|
966
1133
|
|
|
967
1134
|
return (
|
|
968
|
-
<div className="flex flex-col gap-3 p-3">
|
|
1135
|
+
<div className="flex min-w-0 flex-col gap-3 p-3">
|
|
969
1136
|
<div className="flex items-center justify-between">
|
|
970
1137
|
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
971
1138
|
Template JSON
|
|
@@ -980,12 +1147,11 @@ function DataView() {
|
|
|
980
1147
|
Copiar
|
|
981
1148
|
</Button>
|
|
982
1149
|
</div>
|
|
983
|
-
<
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
/>
|
|
1150
|
+
<div className="min-w-0 overflow-hidden rounded-md border border-input bg-transparent">
|
|
1151
|
+
<pre className="max-h-[65vh] min-w-0 overflow-auto p-3 font-mono text-[11px] leading-relaxed whitespace-pre-wrap break-all">
|
|
1152
|
+
<code dangerouslySetInnerHTML={{ __html: highlightedJson }} />
|
|
1153
|
+
</pre>
|
|
1154
|
+
</div>
|
|
989
1155
|
</div>
|
|
990
1156
|
);
|
|
991
1157
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
ResizableHandle,
|
|
5
|
+
ResizablePanel,
|
|
6
|
+
ResizablePanelGroup,
|
|
7
|
+
} from '@/components/ui/resizable';
|
|
3
8
|
import { useApp } from '@hed-hog/next-app-provider';
|
|
4
9
|
import { useTranslations } from 'next-intl';
|
|
5
10
|
import dynamic from 'next/dynamic';
|
|
@@ -96,11 +101,37 @@ export default function TemplateEditorPage() {
|
|
|
96
101
|
: null
|
|
97
102
|
}
|
|
98
103
|
/>
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
<ResizablePanelGroup
|
|
105
|
+
direction="horizontal"
|
|
106
|
+
autoSaveId="lms-certificate-template-editor-layout"
|
|
107
|
+
className="min-h-0 flex-1"
|
|
108
|
+
>
|
|
109
|
+
<ResizablePanel
|
|
110
|
+
defaultSize={20}
|
|
111
|
+
minSize={14}
|
|
112
|
+
maxSize={32}
|
|
113
|
+
className="min-w-0"
|
|
114
|
+
>
|
|
115
|
+
<LeftPanel />
|
|
116
|
+
</ResizablePanel>
|
|
117
|
+
|
|
118
|
+
<ResizableHandle withHandle />
|
|
119
|
+
|
|
120
|
+
<ResizablePanel defaultSize={60} minSize={36} className="min-w-0">
|
|
121
|
+
<CanvasStage isLoading={isLoadingTemplate} />
|
|
122
|
+
</ResizablePanel>
|
|
123
|
+
|
|
124
|
+
<ResizableHandle withHandle />
|
|
125
|
+
|
|
126
|
+
<ResizablePanel
|
|
127
|
+
defaultSize={20}
|
|
128
|
+
minSize={14}
|
|
129
|
+
maxSize={36}
|
|
130
|
+
className="min-w-0"
|
|
131
|
+
>
|
|
132
|
+
<RightPanel />
|
|
133
|
+
</ResizablePanel>
|
|
134
|
+
</ResizablePanelGroup>
|
|
104
135
|
</div>
|
|
105
136
|
);
|
|
106
137
|
}
|
|
@@ -547,7 +547,7 @@ function toCourseFormLevel(level?: string): CourseSheetFormValues['nivel'] {
|
|
|
547
547
|
return 'iniciante';
|
|
548
548
|
}
|
|
549
549
|
function toCourseFormStatus(status?: string): CourseSheetFormValues['status'] {
|
|
550
|
-
if (status === 'published') return '
|
|
550
|
+
if (status === 'published') return 'publicado';
|
|
551
551
|
if (status === 'archived') return 'arquivado';
|
|
552
552
|
return 'rascunho';
|
|
553
553
|
}
|
|
@@ -557,7 +557,7 @@ function toApiCourseLevel(nivel: CourseSheetFormValues['nivel']) {
|
|
|
557
557
|
return 'beginner';
|
|
558
558
|
}
|
|
559
559
|
function toApiCourseStatus(status: CourseSheetFormValues['status']) {
|
|
560
|
-
if (status === '
|
|
560
|
+
if (status === 'publicado') return 'published';
|
|
561
561
|
if (status === 'arquivado') return 'archived';
|
|
562
562
|
return 'draft';
|
|
563
563
|
}
|
|
@@ -7,7 +7,7 @@ export type CourseEditFormValues = {
|
|
|
7
7
|
objetivos: string;
|
|
8
8
|
publicoAlvo: string;
|
|
9
9
|
nivel: 'iniciante' | 'intermediario' | 'avancado';
|
|
10
|
-
status: '
|
|
10
|
+
status: 'publicado' | 'rascunho' | 'arquivado';
|
|
11
11
|
tipoOferta: 'agendado' | 'sob_demanda' | 'hibrido';
|
|
12
12
|
localeId: string;
|
|
13
13
|
categorias: string[];
|