@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.
Files changed (60) hide show
  1. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +18 -8
  2. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +10 -8
  3. package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +5 -9
  4. package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +5 -9
  5. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +15 -14
  6. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +66 -29
  7. package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +4 -2
  8. package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -34
  9. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +1 -1
  10. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +27 -27
  11. package/hedhog/frontend/app/classes/page.tsx.ejs +23 -15
  12. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +2 -2
  13. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +8 -6
  14. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +5 -3
  15. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +1 -1
  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/editor-lesson.tsx.ejs +228 -152
  21. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +21 -19
  22. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +78 -36
  23. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +18 -16
  24. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +13 -11
  25. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +5 -3
  26. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +14 -9
  27. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +42 -25
  28. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +37 -41
  29. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +3 -1
  30. package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +10 -8
  31. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +22 -20
  32. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +3 -3
  33. package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +21 -19
  34. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +34 -36
  35. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +3 -1
  36. package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +7 -5
  37. package/hedhog/frontend/app/enterprise/page.tsx.ejs +106 -54
  38. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +1 -1
  39. package/hedhog/frontend/app/exams/page.tsx.ejs +6 -2
  40. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +79 -59
  41. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +145 -119
  42. package/hedhog/frontend/app/instructors/page.tsx.ejs +75 -54
  43. package/hedhog/frontend/app/paths/page.tsx.ejs +11 -7
  44. package/hedhog/frontend/app/reports/courses/page.tsx.ejs +5 -5
  45. package/hedhog/frontend/app/reports/dashboard/page.tsx.ejs +8 -8
  46. package/hedhog/frontend/app/reports/page.tsx.ejs +7 -7
  47. package/hedhog/frontend/app/reports/students/page.tsx.ejs +6 -6
  48. package/hedhog/frontend/app/training/page.tsx.ejs +5 -5
  49. package/hedhog/frontend/messages/en.json +899 -45
  50. package/hedhog/frontend/messages/pt.json +894 -38
  51. package/hedhog/frontend/widgets/active-classes-kpi.tsx.ejs +1 -1
  52. package/hedhog/frontend/widgets/active-courses-kpi.tsx.ejs +1 -1
  53. package/hedhog/frontend/widgets/approval-rate-kpi.tsx.ejs +1 -1
  54. package/hedhog/frontend/widgets/class-calendar.tsx.ejs +2 -2
  55. package/hedhog/frontend/widgets/completion-rate-kpi.tsx.ejs +1 -1
  56. package/hedhog/frontend/widgets/issued-certificates-kpi.tsx.ejs +1 -1
  57. package/hedhog/frontend/widgets/total-students-kpi.tsx.ejs +1 -1
  58. package/hedhog/table/instructor_qualification.yaml +1 -1
  59. package/hedhog/table/instructor_skill.yaml +1 -1
  60. package/package.json +7 -7
@@ -656,7 +656,7 @@ export function ClassFormSheet({
656
656
  });
657
657
  })
658
658
  .catch(() => {
659
- toast.error('Não foi possível carregar os dados da turma.');
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('Não foi possível cadastrar o curso.');
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('Não foi possível salvar a turma.');
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="Nenhum curso encontrado."
1168
- loadingLabel="Carregando cursos..."
1169
- noResultsLabel="Nenhum curso encontrado."
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="Nenhum professor encontrado."
1695
- noResultsLabel="Nenhum professor encontrado."
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>Logo do Curso</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="Logo"
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 ? 'Substituir logo' : 'Carregar logo'}
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
- Remover
504
+ {t('form.fields.logo.remove')}
503
505
  </Button>
504
506
  )}
505
507
  </div>
506
508
  </div>
507
509
  <FieldDescription>
508
- Imagem opcional para identificar o curso.
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 || 'Nao foi possivel vincular o professor.');
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
- Elementos
67
+ {t('leftPanel.tabs.elements')}
66
68
  </TabsTrigger>
67
69
  <TabsTrigger value="assets" className="flex-1">
68
- Assets
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
- Campos (Fields)
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
- Texto
96
+ {t('leftPanel.sections.text')}
95
97
  </p>
96
98
  <ElementCard
97
99
  icon={<Type className="size-4" />}
98
- label="Texto Fixo"
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
- Formas
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="Retangulo"
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="Linha"
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
- Imagem
126
+ {t('leftPanel.sections.image')}
125
127
  </p>
126
128
  <ElementCard
127
129
  icon={<ImageIcon className="size-4" />}
128
- label="Imagem"
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
- Background do Certificado
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
- Upload Imagem de Fundo
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
- A imagem sera definida como fundo do certificado e travada (nao
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
- Propriedades
48
+ {t('rightPanel.tabs.properties')}
47
49
  </TabsTrigger>
48
50
  <TabsTrigger value="layers" className="flex-1">
49
- Camadas
51
+ {t('rightPanel.tabs.layers')}
50
52
  </TabsTrigger>
51
53
  <TabsTrigger value="data" className="flex-1">
52
- Dados
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">Fonte</Label>
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
- Tamanho ({Math.round(fromPctH(ts?.fontSizePct ?? 2.12))}px)
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">Peso</Label>
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">Normal (400)</SelectItem>
181
- <SelectItem value="600">Semi-Bold (600)</SelectItem>
182
- <SelectItem value="700">Bold (700)</SelectItem>
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">Italico</Label>
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">Cor do Texto</Label>
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">Alinhamento</Label>
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">Esquerda</SelectItem>
223
- <SelectItem value="center">Centro</SelectItem>
224
- <SelectItem value="right">Direita</SelectItem>
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">Contorno do Texto</Label>
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">Cor</Label>
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
- Espessura ({Math.round(fromPctW(stroke?.widthPct ?? 0))}px)
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">Sombra</Label>
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">Cor</Label>
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
- Offset X ({Math.round(fromPctW(shadow?.xPct ?? 0))}px)
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
- Offset Y ({Math.round(fromPctH(shadow?.yPct ?? 0))}px)
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
- Blur ({Math.round(fromPctW(shadow?.blurPct ?? 0))}px)
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">Preenchimento</Label>
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">Cor da Borda</Label>
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
- Espessura Borda ({Math.round(fromPctW(stroke?.widthPct ?? 0.0625))}px)
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('Template invalido, carregado com configuracao padrao.');
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('Nao foi possivel carregar o template selecionado.');
77
+ toast.error(t('page.toasts.loadError'));
76
78
  } finally {
77
79
  setIsLoadingTemplate(false);
78
80
  }