@hed-hog/lms 0.0.329 → 0.0.331
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/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +18 -8
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +10 -8
- package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +5 -9
- package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +5 -9
- package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +15 -14
- package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +66 -29
- package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +4 -2
- package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -34
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +27 -27
- package/hedhog/frontend/app/classes/page.tsx.ejs +23 -15
- package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +2 -2
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +8 -6
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +5 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +9 -7
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +3 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +4 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +24 -23
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +228 -152
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +21 -19
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +78 -36
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +18 -16
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +13 -11
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +5 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +14 -9
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +42 -25
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +37 -41
- package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +3 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +10 -8
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +22 -20
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +3 -3
- package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +21 -19
- package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +34 -36
- package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +3 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +7 -5
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +106 -54
- package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/exams/page.tsx.ejs +6 -2
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +79 -59
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +145 -119
- package/hedhog/frontend/app/instructors/page.tsx.ejs +75 -54
- package/hedhog/frontend/app/paths/page.tsx.ejs +11 -7
- package/hedhog/frontend/app/reports/courses/page.tsx.ejs +5 -5
- package/hedhog/frontend/app/reports/dashboard/page.tsx.ejs +8 -8
- package/hedhog/frontend/app/reports/page.tsx.ejs +7 -7
- package/hedhog/frontend/app/reports/students/page.tsx.ejs +6 -6
- package/hedhog/frontend/app/training/page.tsx.ejs +5 -5
- package/hedhog/frontend/messages/en.json +899 -45
- package/hedhog/frontend/messages/pt.json +894 -38
- package/hedhog/frontend/widgets/active-classes-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/active-courses-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/approval-rate-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/class-calendar.tsx.ejs +2 -2
- package/hedhog/frontend/widgets/completion-rate-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/issued-certificates-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/total-students-kpi.tsx.ejs +1 -1
- package/hedhog/table/instructor_qualification.yaml +1 -1
- package/hedhog/table/instructor_skill.yaml +1 -1
- package/package.json +7 -7
|
@@ -656,7 +656,7 @@ export function ClassFormSheet({
|
|
|
656
656
|
});
|
|
657
657
|
})
|
|
658
658
|
.catch(() => {
|
|
659
|
-
toast.error('
|
|
659
|
+
toast.error(t('messages.classLoadError'));
|
|
660
660
|
})
|
|
661
661
|
.finally(() => {
|
|
662
662
|
setLoading(false);
|
|
@@ -1022,7 +1022,7 @@ export function ClassFormSheet({
|
|
|
1022
1022
|
setCourseSheetOpen(false);
|
|
1023
1023
|
toast.success(courseSheetT('toasts.courseCreated'));
|
|
1024
1024
|
} catch {
|
|
1025
|
-
toast.error('
|
|
1025
|
+
toast.error(t('messages.classCreateCourseError'));
|
|
1026
1026
|
} finally {
|
|
1027
1027
|
setSavingCourse(false);
|
|
1028
1028
|
}
|
|
@@ -1096,7 +1096,7 @@ export function ClassFormSheet({
|
|
|
1096
1096
|
onSaved?.();
|
|
1097
1097
|
onSuccess?.();
|
|
1098
1098
|
} catch {
|
|
1099
|
-
toast.error('
|
|
1099
|
+
toast.error(t('messages.classSaveError'));
|
|
1100
1100
|
} finally {
|
|
1101
1101
|
setSaving(false);
|
|
1102
1102
|
}
|
|
@@ -1164,9 +1164,15 @@ export function ClassFormSheet({
|
|
|
1164
1164
|
entityLabel={t('form.fields.course.label')}
|
|
1165
1165
|
initialSelectedLabel={selectedCourseTitle}
|
|
1166
1166
|
searchPlaceholder={t('form.fields.course.placeholder')}
|
|
1167
|
-
emptyStateDescription=
|
|
1168
|
-
|
|
1169
|
-
|
|
1167
|
+
emptyStateDescription={t(
|
|
1168
|
+
'components.entityPicker.courses.empty'
|
|
1169
|
+
)}
|
|
1170
|
+
loadingLabel={t(
|
|
1171
|
+
'components.entityPicker.courses.loading'
|
|
1172
|
+
)}
|
|
1173
|
+
noResultsLabel={t(
|
|
1174
|
+
'components.entityPicker.courses.empty'
|
|
1175
|
+
)}
|
|
1170
1176
|
showCreateButton={false}
|
|
1171
1177
|
onChange={(value, option) => {
|
|
1172
1178
|
const cId =
|
|
@@ -1691,8 +1697,12 @@ export function ClassFormSheet({
|
|
|
1691
1697
|
placeholder={t('form.fields.professor.placeholder')}
|
|
1692
1698
|
initialSelectedLabel={watchedFormValues.professor ?? ''}
|
|
1693
1699
|
searchPlaceholder={t('form.fields.professor.placeholder')}
|
|
1694
|
-
emptyStateDescription=
|
|
1695
|
-
|
|
1700
|
+
emptyStateDescription={t(
|
|
1701
|
+
'components.entityPicker.instructors.empty'
|
|
1702
|
+
)}
|
|
1703
|
+
noResultsLabel={t(
|
|
1704
|
+
'components.entityPicker.instructors.empty'
|
|
1705
|
+
)}
|
|
1696
1706
|
showCreateButton={false}
|
|
1697
1707
|
clearable={false}
|
|
1698
1708
|
getOptionValue={(opt) => opt.id}
|
|
@@ -454,12 +454,12 @@ export function CourseFormSheet({
|
|
|
454
454
|
</Field>
|
|
455
455
|
|
|
456
456
|
<Field>
|
|
457
|
-
<FieldLabel>
|
|
457
|
+
<FieldLabel>{t('form.fields.logo.label')}</FieldLabel>
|
|
458
458
|
<div className="flex items-start gap-4">
|
|
459
459
|
{logoPreviewUrl ? (
|
|
460
460
|
<img
|
|
461
461
|
src={logoPreviewUrl}
|
|
462
|
-
alt=
|
|
462
|
+
alt={t('form.fields.logo.alt')}
|
|
463
463
|
className="size-16 shrink-0 rounded-lg border object-cover"
|
|
464
464
|
/>
|
|
465
465
|
) : (
|
|
@@ -488,7 +488,9 @@ export function CourseFormSheet({
|
|
|
488
488
|
) : (
|
|
489
489
|
<Upload className="size-3.5" />
|
|
490
490
|
)}
|
|
491
|
-
{logoPreviewUrl
|
|
491
|
+
{logoPreviewUrl
|
|
492
|
+
? t('form.fields.logo.replace')
|
|
493
|
+
: t('form.fields.logo.upload')}
|
|
492
494
|
</Button>
|
|
493
495
|
{logoPreviewUrl && (
|
|
494
496
|
<Button
|
|
@@ -499,13 +501,13 @@ export function CourseFormSheet({
|
|
|
499
501
|
className="gap-2 text-destructive hover:text-destructive"
|
|
500
502
|
>
|
|
501
503
|
<X className="size-3.5" />
|
|
502
|
-
|
|
504
|
+
{t('form.fields.logo.remove')}
|
|
503
505
|
</Button>
|
|
504
506
|
)}
|
|
505
507
|
</div>
|
|
506
508
|
</div>
|
|
507
509
|
<FieldDescription>
|
|
508
|
-
|
|
510
|
+
{t('form.fields.logo.description')}
|
|
509
511
|
</FieldDescription>
|
|
510
512
|
</Field>
|
|
511
513
|
|
|
@@ -524,14 +526,14 @@ export function CourseFormSheet({
|
|
|
524
526
|
</Field>
|
|
525
527
|
|
|
526
528
|
<Field>
|
|
527
|
-
<FieldLabel htmlFor="descricao">
|
|
529
|
+
<FieldLabel htmlFor="descricao">
|
|
528
530
|
{t('form.fields.description.label')}
|
|
529
531
|
</FieldLabel>
|
|
530
532
|
<Textarea
|
|
531
|
-
id="descricao"
|
|
533
|
+
id="descricao"
|
|
532
534
|
rows={3}
|
|
533
535
|
placeholder={t('form.fields.description.placeholder')}
|
|
534
|
-
{...form.register('descricao')}
|
|
536
|
+
{...form.register('descricao')}
|
|
535
537
|
/>
|
|
536
538
|
<FieldError>{form.formState.errors.descricao?.message}</FieldError>
|
|
537
539
|
</Field>
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
Person,
|
|
8
8
|
} from '@/app/(app)/(libraries)/contact/person/_components/person-types';
|
|
9
9
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
10
|
+
import { useTranslations } from 'next-intl';
|
|
10
11
|
import { useMemo } from 'react';
|
|
11
12
|
|
|
12
13
|
type CreatedInstructor = {
|
|
@@ -63,6 +64,7 @@ export function CreateLmsPersonSheet({
|
|
|
63
64
|
defaultQualificationSlugs,
|
|
64
65
|
}: CreateLmsPersonSheetProps) {
|
|
65
66
|
const { request, currentLocaleCode } = useApp();
|
|
67
|
+
const t = useTranslations('lms.CreateLmsPersonSheet');
|
|
66
68
|
|
|
67
69
|
const qualificationSlugs = useMemo(
|
|
68
70
|
() =>
|
|
@@ -111,16 +113,10 @@ export function CreateLmsPersonSheet({
|
|
|
111
113
|
|
|
112
114
|
const handleSuccess = async (person?: Person) => {
|
|
113
115
|
if (!person?.id) {
|
|
114
|
-
throw new Error(
|
|
115
|
-
errorMessage || 'Nao foi possivel localizar a pessoa cadastrada.'
|
|
116
|
-
);
|
|
116
|
+
throw new Error(errorMessage || t('errors.personNotFound'));
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const email = getPrimaryContactValue(
|
|
120
|
-
person,
|
|
121
|
-
['EMAIL'],
|
|
122
|
-
contactTypesById
|
|
123
|
-
);
|
|
119
|
+
const email = getPrimaryContactValue(person, ['EMAIL'], contactTypesById);
|
|
124
120
|
const phone = getPrimaryContactValue(
|
|
125
121
|
person,
|
|
126
122
|
['PHONE', 'MOBILE', 'WHATSAPP'],
|
|
@@ -142,7 +138,7 @@ export function CreateLmsPersonSheet({
|
|
|
142
138
|
});
|
|
143
139
|
|
|
144
140
|
if (!response.data?.id || !response.data?.name) {
|
|
145
|
-
throw new Error(errorMessage || '
|
|
141
|
+
throw new Error(errorMessage || t('errors.linkInstructorFailed'));
|
|
146
142
|
}
|
|
147
143
|
|
|
148
144
|
await onCreated?.(response.data);
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
Person,
|
|
8
8
|
} from '@/app/(app)/(libraries)/contact/person/_components/person-types';
|
|
9
9
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
10
|
+
import { useTranslations } from 'next-intl';
|
|
10
11
|
|
|
11
12
|
type CreateLmsStudentPersonSheetProps = {
|
|
12
13
|
open: boolean;
|
|
@@ -45,6 +46,7 @@ export function CreateLmsStudentPersonSheet({
|
|
|
45
46
|
alreadyEnrolledMessage,
|
|
46
47
|
}: CreateLmsStudentPersonSheetProps) {
|
|
47
48
|
const { request, currentLocaleCode } = useApp();
|
|
49
|
+
const t = useTranslations('lms.CreateLmsStudentPersonSheet');
|
|
48
50
|
|
|
49
51
|
const { data: contactTypes = [] } = useQuery<ContactTypeOption[]>({
|
|
50
52
|
queryKey: ['contact-person-contact-types', currentLocaleCode],
|
|
@@ -74,9 +76,7 @@ export function CreateLmsStudentPersonSheet({
|
|
|
74
76
|
|
|
75
77
|
const handleSuccess = async (person?: Person) => {
|
|
76
78
|
if (!person?.id) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
errorMessage || 'Nao foi possivel localizar a pessoa cadastrada.'
|
|
79
|
-
);
|
|
79
|
+
throw new Error(errorMessage || t('errors.personNotFound'));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
try {
|
|
@@ -91,14 +91,10 @@ export function CreateLmsStudentPersonSheet({
|
|
|
91
91
|
const message = getErrorMessage(error);
|
|
92
92
|
|
|
93
93
|
if (message?.toLowerCase().includes('already enrolled')) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
alreadyEnrolledMessage || 'Esta pessoa ja esta matriculada na turma.'
|
|
96
|
-
);
|
|
94
|
+
throw new Error(alreadyEnrolledMessage || t('errors.alreadyEnrolled'));
|
|
97
95
|
}
|
|
98
96
|
|
|
99
|
-
throw new Error(
|
|
100
|
-
message || errorMessage || 'Nao foi possivel matricular o aluno.'
|
|
101
|
-
);
|
|
97
|
+
throw new Error(message || errorMessage || t('errors.enrollFailed'));
|
|
102
98
|
}
|
|
103
99
|
|
|
104
100
|
await onEnrolled?.(person);
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
Type,
|
|
13
13
|
UploadCloud,
|
|
14
14
|
} from 'lucide-react';
|
|
15
|
+
import { useTranslations } from 'next-intl';
|
|
15
16
|
import { useCallback, useRef } from 'react';
|
|
16
17
|
import { getCanvasAPI } from '../../_lib/editor/canvasInstance';
|
|
17
18
|
import {
|
|
@@ -30,6 +31,7 @@ interface DragPayload {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export default function LeftPanel() {
|
|
34
|
+
const t = useTranslations('lms.CertificateTemplateEditor');
|
|
33
35
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
34
36
|
|
|
35
37
|
const add = useCallback(
|
|
@@ -62,10 +64,10 @@ export default function LeftPanel() {
|
|
|
62
64
|
<Tabs defaultValue="elements" className="flex flex-1 flex-col">
|
|
63
65
|
<TabsList className="mx-3 mt-3 w-auto">
|
|
64
66
|
<TabsTrigger value="elements" className="flex-1">
|
|
65
|
-
|
|
67
|
+
{t('leftPanel.tabs.elements')}
|
|
66
68
|
</TabsTrigger>
|
|
67
69
|
<TabsTrigger value="assets" className="flex-1">
|
|
68
|
-
|
|
70
|
+
{t('leftPanel.tabs.assets')}
|
|
69
71
|
</TabsTrigger>
|
|
70
72
|
</TabsList>
|
|
71
73
|
|
|
@@ -75,7 +77,7 @@ export default function LeftPanel() {
|
|
|
75
77
|
<div className="flex flex-col gap-2 p-3">
|
|
76
78
|
{/* fields */}
|
|
77
79
|
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
78
|
-
|
|
80
|
+
{t('leftPanel.sections.fields')}
|
|
79
81
|
</p>
|
|
80
82
|
<div className="grid grid-cols-2 gap-2">
|
|
81
83
|
{FIELD_KEYS.map((k) => (
|
|
@@ -91,29 +93,29 @@ export default function LeftPanel() {
|
|
|
91
93
|
|
|
92
94
|
{/* static text */}
|
|
93
95
|
<p className="mt-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
94
|
-
|
|
96
|
+
{t('leftPanel.sections.text')}
|
|
95
97
|
</p>
|
|
96
98
|
<ElementCard
|
|
97
99
|
icon={<Type className="size-4" />}
|
|
98
|
-
label=
|
|
100
|
+
label={t('leftPanel.elements.staticText')}
|
|
99
101
|
onClick={() => add('staticText')}
|
|
100
102
|
dragPayload={{ type: 'staticText' }}
|
|
101
103
|
/>
|
|
102
104
|
|
|
103
105
|
{/* shapes */}
|
|
104
106
|
<p className="mt-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
105
|
-
|
|
107
|
+
{t('leftPanel.sections.shapes')}
|
|
106
108
|
</p>
|
|
107
109
|
<div className="grid grid-cols-2 gap-2">
|
|
108
110
|
<ElementCard
|
|
109
111
|
icon={<RectangleHorizontal className="size-4" />}
|
|
110
|
-
label=
|
|
112
|
+
label={t('leftPanel.elements.rectangle')}
|
|
111
113
|
onClick={() => add('shape', { shape: 'rect' })}
|
|
112
114
|
dragPayload={{ type: 'shape', shape: 'rect' }}
|
|
113
115
|
/>
|
|
114
116
|
<ElementCard
|
|
115
117
|
icon={<Minus className="size-4" />}
|
|
116
|
-
label=
|
|
118
|
+
label={t('leftPanel.elements.line')}
|
|
117
119
|
onClick={() => add('shape', { shape: 'line' })}
|
|
118
120
|
dragPayload={{ type: 'shape', shape: 'line' }}
|
|
119
121
|
/>
|
|
@@ -121,11 +123,11 @@ export default function LeftPanel() {
|
|
|
121
123
|
|
|
122
124
|
{/* image */}
|
|
123
125
|
<p className="mt-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
124
|
-
|
|
126
|
+
{t('leftPanel.sections.image')}
|
|
125
127
|
</p>
|
|
126
128
|
<ElementCard
|
|
127
129
|
icon={<ImageIcon className="size-4" />}
|
|
128
|
-
label=
|
|
130
|
+
label={t('leftPanel.elements.image')}
|
|
129
131
|
onClick={() => add('image')}
|
|
130
132
|
dragPayload={{ type: 'image' }}
|
|
131
133
|
/>
|
|
@@ -137,7 +139,7 @@ export default function LeftPanel() {
|
|
|
137
139
|
<TabsContent value="assets" className="flex-1 overflow-hidden">
|
|
138
140
|
<div className="flex flex-col gap-4 p-3">
|
|
139
141
|
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
140
|
-
|
|
142
|
+
{t('leftPanel.background.title')}
|
|
141
143
|
</p>
|
|
142
144
|
<Button
|
|
143
145
|
variant="outline"
|
|
@@ -146,7 +148,7 @@ export default function LeftPanel() {
|
|
|
146
148
|
>
|
|
147
149
|
<UploadCloud className="size-6 text-muted-foreground" />
|
|
148
150
|
<span className="text-xs text-muted-foreground">
|
|
149
|
-
|
|
151
|
+
{t('leftPanel.background.upload')}
|
|
150
152
|
</span>
|
|
151
153
|
</Button>
|
|
152
154
|
<input
|
|
@@ -157,8 +159,7 @@ export default function LeftPanel() {
|
|
|
157
159
|
onChange={handleBgUpload}
|
|
158
160
|
/>
|
|
159
161
|
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
|
160
|
-
|
|
161
|
-
selecionavel).
|
|
162
|
+
{t('leftPanel.background.description')}
|
|
162
163
|
</p>
|
|
163
164
|
</div>
|
|
164
165
|
</TabsContent>
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
Trash2,
|
|
27
27
|
Unlock,
|
|
28
28
|
} from 'lucide-react';
|
|
29
|
+
import { useTranslations } from 'next-intl';
|
|
29
30
|
import { useCallback, useRef, useState } from 'react';
|
|
30
31
|
import { toast } from 'sonner';
|
|
31
32
|
import { getCanvasAPI } from '../../_lib/editor/canvasInstance';
|
|
@@ -35,6 +36,7 @@ import { FONT_FAMILIES, getObjectLabel } from '../../_lib/editor/types';
|
|
|
35
36
|
import { useTemplateStore } from '../../_lib/store/useTemplateStore';
|
|
36
37
|
|
|
37
38
|
export default function RightPanel() {
|
|
39
|
+
const t = useTranslations('lms.CertificateTemplateEditor');
|
|
38
40
|
return (
|
|
39
41
|
<aside className="flex w-75 shrink-0 flex-col border-l border-border bg-background">
|
|
40
42
|
<Tabs
|
|
@@ -43,13 +45,13 @@ export default function RightPanel() {
|
|
|
43
45
|
>
|
|
44
46
|
<TabsList className="mx-3 mt-3 w-auto">
|
|
45
47
|
<TabsTrigger value="props" className="flex-1">
|
|
46
|
-
|
|
48
|
+
{t('rightPanel.tabs.properties')}
|
|
47
49
|
</TabsTrigger>
|
|
48
50
|
<TabsTrigger value="layers" className="flex-1">
|
|
49
|
-
|
|
51
|
+
{t('rightPanel.tabs.layers')}
|
|
50
52
|
</TabsTrigger>
|
|
51
53
|
<TabsTrigger value="data" className="flex-1">
|
|
52
|
-
|
|
54
|
+
{t('rightPanel.tabs.data')}
|
|
53
55
|
</TabsTrigger>
|
|
54
56
|
</TabsList>
|
|
55
57
|
|
|
@@ -80,6 +82,7 @@ export default function RightPanel() {
|
|
|
80
82
|
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
|
81
83
|
|
|
82
84
|
function PropertiesInspector() {
|
|
85
|
+
const t = useTranslations('lms.CertificateTemplateEditor');
|
|
83
86
|
const selectedId = useTemplateStore((s) => s.selectedObjectId);
|
|
84
87
|
const objects = useTemplateStore((s) => s.template.objects);
|
|
85
88
|
const obj = objects.find((o) => o.id === selectedId);
|
|
@@ -96,9 +99,7 @@ function PropertiesInspector() {
|
|
|
96
99
|
return (
|
|
97
100
|
<div className="flex flex-col items-center justify-center gap-2 p-8 text-center text-muted-foreground">
|
|
98
101
|
<MousePointer2 className="size-8" />
|
|
99
|
-
<p className="text-sm">
|
|
100
|
-
Selecione um objeto no canvas para editar suas propriedades.
|
|
101
|
-
</p>
|
|
102
|
+
<p className="text-sm">{t('rightPanel.emptySelection')}</p>
|
|
102
103
|
</div>
|
|
103
104
|
);
|
|
104
105
|
}
|
|
@@ -131,12 +132,13 @@ function TextProperties({
|
|
|
131
132
|
obj: TemplateObject;
|
|
132
133
|
setProp: (p: Record<string, unknown>) => void;
|
|
133
134
|
}) {
|
|
135
|
+
const t = useTranslations('lms.CertificateTemplateEditor');
|
|
134
136
|
const ts = obj.textStyle;
|
|
135
137
|
|
|
136
138
|
return (
|
|
137
139
|
<>
|
|
138
140
|
<div className="flex flex-col gap-1.5">
|
|
139
|
-
<Label className="text-xs">
|
|
141
|
+
<Label className="text-xs">{t('rightPanel.text.font')}</Label>
|
|
140
142
|
<Select
|
|
141
143
|
value={ts?.fontFamily ?? 'Inter'}
|
|
142
144
|
onValueChange={(v) => setProp({ fontFamily: v })}
|
|
@@ -156,7 +158,9 @@ function TextProperties({
|
|
|
156
158
|
|
|
157
159
|
<div className="flex flex-col gap-1.5">
|
|
158
160
|
<Label className="text-xs">
|
|
159
|
-
|
|
161
|
+
{t('rightPanel.text.size', {
|
|
162
|
+
size: Math.round(fromPctH(ts?.fontSizePct ?? 2.12)),
|
|
163
|
+
})}
|
|
160
164
|
</Label>
|
|
161
165
|
<Slider
|
|
162
166
|
min={8}
|
|
@@ -168,7 +172,7 @@ function TextProperties({
|
|
|
168
172
|
</div>
|
|
169
173
|
|
|
170
174
|
<div className="flex flex-col gap-1.5">
|
|
171
|
-
<Label className="text-xs">
|
|
175
|
+
<Label className="text-xs">{t('rightPanel.text.weight')}</Label>
|
|
172
176
|
<Select
|
|
173
177
|
value={String(ts?.fontWeight ?? 400)}
|
|
174
178
|
onValueChange={(v) => setProp({ fontWeight: v })}
|
|
@@ -177,15 +181,21 @@ function TextProperties({
|
|
|
177
181
|
<SelectValue />
|
|
178
182
|
</SelectTrigger>
|
|
179
183
|
<SelectContent>
|
|
180
|
-
<SelectItem value="400">
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
<SelectItem value="400">
|
|
185
|
+
{t('rightPanel.text.weights.normal')}
|
|
186
|
+
</SelectItem>
|
|
187
|
+
<SelectItem value="600">
|
|
188
|
+
{t('rightPanel.text.weights.semiBold')}
|
|
189
|
+
</SelectItem>
|
|
190
|
+
<SelectItem value="700">
|
|
191
|
+
{t('rightPanel.text.weights.bold')}
|
|
192
|
+
</SelectItem>
|
|
183
193
|
</SelectContent>
|
|
184
194
|
</Select>
|
|
185
195
|
</div>
|
|
186
196
|
|
|
187
197
|
<div className="flex items-center justify-between">
|
|
188
|
-
<Label className="text-xs">
|
|
198
|
+
<Label className="text-xs">{t('rightPanel.text.italic')}</Label>
|
|
189
199
|
<Switch
|
|
190
200
|
checked={ts?.italic ?? false}
|
|
191
201
|
onCheckedChange={(v) =>
|
|
@@ -195,7 +205,7 @@ function TextProperties({
|
|
|
195
205
|
</div>
|
|
196
206
|
|
|
197
207
|
<div className="flex flex-col gap-1.5">
|
|
198
|
-
<Label className="text-xs">
|
|
208
|
+
<Label className="text-xs">{t('rightPanel.text.color')}</Label>
|
|
199
209
|
<div className="flex items-center gap-2">
|
|
200
210
|
<input
|
|
201
211
|
type="color"
|
|
@@ -210,7 +220,7 @@ function TextProperties({
|
|
|
210
220
|
</div>
|
|
211
221
|
|
|
212
222
|
<div className="flex flex-col gap-1.5">
|
|
213
|
-
<Label className="text-xs">
|
|
223
|
+
<Label className="text-xs">{t('rightPanel.text.alignment')}</Label>
|
|
214
224
|
<Select
|
|
215
225
|
value={ts?.align ?? 'left'}
|
|
216
226
|
onValueChange={(v) => setProp({ textAlign: v })}
|
|
@@ -219,9 +229,15 @@ function TextProperties({
|
|
|
219
229
|
<SelectValue />
|
|
220
230
|
</SelectTrigger>
|
|
221
231
|
<SelectContent>
|
|
222
|
-
<SelectItem value="left">
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
<SelectItem value="left">
|
|
233
|
+
{t('rightPanel.text.alignments.left')}
|
|
234
|
+
</SelectItem>
|
|
235
|
+
<SelectItem value="center">
|
|
236
|
+
{t('rightPanel.text.alignments.center')}
|
|
237
|
+
</SelectItem>
|
|
238
|
+
<SelectItem value="right">
|
|
239
|
+
{t('rightPanel.text.alignments.right')}
|
|
240
|
+
</SelectItem>
|
|
225
241
|
</SelectContent>
|
|
226
242
|
</Select>
|
|
227
243
|
</div>
|
|
@@ -237,13 +253,16 @@ function TextStrokeProperties({
|
|
|
237
253
|
obj: TemplateObject;
|
|
238
254
|
setProp: (p: Record<string, unknown>) => void;
|
|
239
255
|
}) {
|
|
256
|
+
const t = useTranslations('lms.CertificateTemplateEditor');
|
|
240
257
|
const stroke = obj.effects?.stroke;
|
|
241
258
|
const hasStroke = !!stroke && fromPctW(stroke.widthPct) > 0;
|
|
242
259
|
|
|
243
260
|
return (
|
|
244
261
|
<div className="flex flex-col gap-2 rounded-md border border-border p-2.5">
|
|
245
262
|
<div className="flex items-center justify-between">
|
|
246
|
-
<Label className="text-xs font-semibold">
|
|
263
|
+
<Label className="text-xs font-semibold">
|
|
264
|
+
{t('rightPanel.stroke.title')}
|
|
265
|
+
</Label>
|
|
247
266
|
<Switch
|
|
248
267
|
checked={hasStroke}
|
|
249
268
|
onCheckedChange={(on) => {
|
|
@@ -258,7 +277,9 @@ function TextStrokeProperties({
|
|
|
258
277
|
{hasStroke && (
|
|
259
278
|
<>
|
|
260
279
|
<div className="flex items-center gap-2">
|
|
261
|
-
<Label className="w-12 text-xs">
|
|
280
|
+
<Label className="w-12 text-xs">
|
|
281
|
+
{t('rightPanel.common.color')}
|
|
282
|
+
</Label>
|
|
262
283
|
<input
|
|
263
284
|
type="color"
|
|
264
285
|
value={stroke?.color ?? '#000000'}
|
|
@@ -271,7 +292,9 @@ function TextStrokeProperties({
|
|
|
271
292
|
</div>
|
|
272
293
|
<div className="flex flex-col gap-1">
|
|
273
294
|
<Label className="text-xs">
|
|
274
|
-
|
|
295
|
+
{t('rightPanel.stroke.width', {
|
|
296
|
+
size: Math.round(fromPctW(stroke?.widthPct ?? 0)),
|
|
297
|
+
})}
|
|
275
298
|
</Label>
|
|
276
299
|
<Slider
|
|
277
300
|
min={1}
|
|
@@ -295,6 +318,7 @@ function TextShadowProperties({
|
|
|
295
318
|
obj: TemplateObject;
|
|
296
319
|
setProp: (p: Record<string, unknown>) => void;
|
|
297
320
|
}) {
|
|
321
|
+
const t = useTranslations('lms.CertificateTemplateEditor');
|
|
298
322
|
const shadow = obj.effects?.shadow;
|
|
299
323
|
const hasShadow = !!shadow;
|
|
300
324
|
|
|
@@ -321,7 +345,9 @@ function TextShadowProperties({
|
|
|
321
345
|
return (
|
|
322
346
|
<div className="flex flex-col gap-2 rounded-md border border-border p-2.5">
|
|
323
347
|
<div className="flex items-center justify-between">
|
|
324
|
-
<Label className="text-xs font-semibold">
|
|
348
|
+
<Label className="text-xs font-semibold">
|
|
349
|
+
{t('rightPanel.shadow.title')}
|
|
350
|
+
</Label>
|
|
325
351
|
<Switch
|
|
326
352
|
checked={hasShadow}
|
|
327
353
|
onCheckedChange={(on) => {
|
|
@@ -338,7 +364,9 @@ function TextShadowProperties({
|
|
|
338
364
|
{hasShadow && (
|
|
339
365
|
<>
|
|
340
366
|
<div className="flex items-center gap-2">
|
|
341
|
-
<Label className="w-12 text-xs">
|
|
367
|
+
<Label className="w-12 text-xs">
|
|
368
|
+
{t('rightPanel.common.color')}
|
|
369
|
+
</Label>
|
|
342
370
|
<input
|
|
343
371
|
type="color"
|
|
344
372
|
value={shadow?.color?.startsWith('#') ? shadow.color : '#000000'}
|
|
@@ -348,7 +376,9 @@ function TextShadowProperties({
|
|
|
348
376
|
</div>
|
|
349
377
|
<div className="flex flex-col gap-1">
|
|
350
378
|
<Label className="text-xs">
|
|
351
|
-
|
|
379
|
+
{t('rightPanel.shadow.offsetX', {
|
|
380
|
+
size: Math.round(fromPctW(shadow?.xPct ?? 0)),
|
|
381
|
+
})}
|
|
352
382
|
</Label>
|
|
353
383
|
<Slider
|
|
354
384
|
min={-20}
|
|
@@ -360,7 +390,9 @@ function TextShadowProperties({
|
|
|
360
390
|
</div>
|
|
361
391
|
<div className="flex flex-col gap-1">
|
|
362
392
|
<Label className="text-xs">
|
|
363
|
-
|
|
393
|
+
{t('rightPanel.shadow.offsetY', {
|
|
394
|
+
size: Math.round(fromPctH(shadow?.yPct ?? 0)),
|
|
395
|
+
})}
|
|
364
396
|
</Label>
|
|
365
397
|
<Slider
|
|
366
398
|
min={-20}
|
|
@@ -372,7 +404,9 @@ function TextShadowProperties({
|
|
|
372
404
|
</div>
|
|
373
405
|
<div className="flex flex-col gap-1">
|
|
374
406
|
<Label className="text-xs">
|
|
375
|
-
|
|
407
|
+
{t('rightPanel.shadow.blur', {
|
|
408
|
+
size: Math.round(fromPctW(shadow?.blurPct ?? 0)),
|
|
409
|
+
})}
|
|
376
410
|
</Label>
|
|
377
411
|
<Slider
|
|
378
412
|
min={0}
|
|
@@ -396,13 +430,14 @@ function ShapeProperties({
|
|
|
396
430
|
obj: TemplateObject;
|
|
397
431
|
setProp: (p: Record<string, unknown>) => void;
|
|
398
432
|
}) {
|
|
433
|
+
const t = useTranslations('lms.CertificateTemplateEditor');
|
|
399
434
|
const stroke = obj.effects?.stroke;
|
|
400
435
|
|
|
401
436
|
return (
|
|
402
437
|
<>
|
|
403
438
|
{obj.shape !== 'line' && (
|
|
404
439
|
<div className="flex flex-col gap-1.5">
|
|
405
|
-
<Label className="text-xs">
|
|
440
|
+
<Label className="text-xs">{t('rightPanel.shape.fill')}</Label>
|
|
406
441
|
<div className="flex items-center gap-2">
|
|
407
442
|
<input
|
|
408
443
|
type="color"
|
|
@@ -418,7 +453,7 @@ function ShapeProperties({
|
|
|
418
453
|
)}
|
|
419
454
|
|
|
420
455
|
<div className="flex flex-col gap-1.5">
|
|
421
|
-
<Label className="text-xs">
|
|
456
|
+
<Label className="text-xs">{t('rightPanel.shape.borderColor')}</Label>
|
|
422
457
|
<div className="flex items-center gap-2">
|
|
423
458
|
<input
|
|
424
459
|
type="color"
|
|
@@ -434,7 +469,9 @@ function ShapeProperties({
|
|
|
434
469
|
|
|
435
470
|
<div className="flex flex-col gap-1.5">
|
|
436
471
|
<Label className="text-xs">
|
|
437
|
-
|
|
472
|
+
{t('rightPanel.shape.borderWidth', {
|
|
473
|
+
size: Math.round(fromPctW(stroke?.widthPct ?? 0.0625)),
|
|
474
|
+
})}
|
|
438
475
|
</Label>
|
|
439
476
|
<Slider
|
|
440
477
|
min={0}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useApp } from '@hed-hog/next-app-provider';
|
|
4
|
+
import { useTranslations } from 'next-intl';
|
|
4
5
|
import dynamic from 'next/dynamic';
|
|
5
6
|
import { useSearchParams } from 'next/navigation';
|
|
6
7
|
import { useEffect, useMemo, useState } from 'react';
|
|
@@ -26,6 +27,7 @@ type CertificateTemplateResponse = {
|
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
export default function TemplateEditorPage() {
|
|
30
|
+
const t = useTranslations('lms.CertificateTemplateEditor');
|
|
29
31
|
const { request } = useApp();
|
|
30
32
|
const searchParams = useSearchParams();
|
|
31
33
|
const templateId = useMemo(() => {
|
|
@@ -65,14 +67,14 @@ export default function TemplateEditorPage() {
|
|
|
65
67
|
const fallback = createDefaultTemplate();
|
|
66
68
|
fallback.name = templateRecord.name;
|
|
67
69
|
setTemplate(fallback);
|
|
68
|
-
toast.error('
|
|
70
|
+
toast.error(t('page.toasts.invalidTemplate'));
|
|
69
71
|
return;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
parsedContent.name = templateRecord.name;
|
|
73
75
|
setTemplate(parsedContent);
|
|
74
76
|
} catch {
|
|
75
|
-
toast.error('
|
|
77
|
+
toast.error(t('page.toasts.loadError'));
|
|
76
78
|
} finally {
|
|
77
79
|
setIsLoadingTemplate(false);
|
|
78
80
|
}
|