@hed-hog/lms 0.0.261 → 0.0.266

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.
@@ -80,6 +80,7 @@ import {
80
80
  Users,
81
81
  X,
82
82
  } from 'lucide-react';
83
+ import { useTranslations } from 'next-intl';
83
84
  import { usePathname, useRouter } from 'next/navigation';
84
85
  import { useEffect, useMemo, useState } from 'react';
85
86
  import { Controller, useForm } from 'react-hook-form';
@@ -117,71 +118,54 @@ interface Curso {
117
118
 
118
119
  // ── Schema ───────────────────────────────────────────────────────────────────
119
120
 
120
- const cursoSchema = z.object({
121
- codigo: z
122
- .string()
123
- .min(2, 'Codigo deve ter pelo menos 2 caracteres')
124
- .max(16, 'Codigo deve ter no maximo 16 caracteres')
125
- .regex(/^[A-Z0-9-]+$/i, 'Apenas letras, numeros e hifens'),
126
- nomeInterno: z
127
- .string()
128
- .min(3, 'Nome interno deve ter pelo menos 3 caracteres'),
129
- tituloComercial: z
130
- .string()
131
- .min(3, 'Titulo comercial deve ter pelo menos 3 caracteres'),
132
- descricao: z.string().min(10, 'Descricao deve ter pelo menos 10 caracteres'),
133
- nivel: z.enum(['iniciante', 'intermediario', 'avancado'], {
134
- errorMap: () => ({ message: 'Selecione um nivel' }),
135
- }),
136
- status: z.enum(['ativo', 'rascunho', 'arquivado'], {
137
- errorMap: () => ({ message: 'Selecione um status' }),
138
- }),
139
- categorias: z.array(z.string()).min(1, 'Selecione pelo menos uma categoria'),
140
- destaque: z.boolean(),
141
- certificado: z.boolean(),
142
- listado: z.boolean(),
143
- });
121
+ const createCursoSchema = (t: (key: string) => string) =>
122
+ z.object({
123
+ codigo: z
124
+ .string()
125
+ .min(2, t('form.validation.codeMinLength'))
126
+ .max(16, t('form.validation.codeMaxLength'))
127
+ .regex(/^[A-Z0-9-]+$/i, t('form.validation.codePattern')),
128
+ nomeInterno: z.string().min(3, t('form.validation.internalNameMinLength')),
129
+ tituloComercial: z
130
+ .string()
131
+ .min(3, t('form.validation.commercialTitleMinLength')),
132
+ descricao: z.string().min(10, t('form.validation.descriptionMinLength')),
133
+ nivel: z.enum(['iniciante', 'intermediario', 'avancado'], {
134
+ errorMap: () => ({ message: t('form.validation.levelRequired') }),
135
+ }),
136
+ status: z.enum(['ativo', 'rascunho', 'arquivado'], {
137
+ errorMap: () => ({ message: t('form.validation.statusRequired') }),
138
+ }),
139
+ categorias: z
140
+ .array(z.string())
141
+ .min(1, t('form.validation.categoriesRequired')),
142
+ destaque: z.boolean(),
143
+ certificado: z.boolean(),
144
+ listado: z.boolean(),
145
+ });
144
146
 
145
- type CursoForm = z.infer<typeof cursoSchema>;
147
+ type CursoForm = z.infer<ReturnType<typeof createCursoSchema>>;
146
148
 
147
149
  // ── Constants ────────────────────────────────────────────────────────────────
148
150
 
149
151
  const CATEGORIAS = [
150
- 'Tecnologia',
151
- 'Design',
152
- 'Gestao',
153
- 'Marketing',
154
- 'Financas',
155
- 'Saude',
156
- 'Idiomas',
157
- 'Direito',
158
- ];
159
-
160
- const NIVEIS = [
161
- { value: 'iniciante', label: 'Iniciante' },
162
- { value: 'intermediario', label: 'Intermediario' },
163
- { value: 'avancado', label: 'Avancado' },
164
- ];
165
-
166
- const STATUS_OPTIONS = [
167
- { value: 'ativo', label: 'Ativo' },
168
- { value: 'rascunho', label: 'Rascunho' },
169
- { value: 'arquivado', label: 'Arquivado' },
170
- ];
152
+ 'technology',
153
+ 'design',
154
+ 'management',
155
+ 'marketing',
156
+ 'finance',
157
+ 'health',
158
+ 'languages',
159
+ 'law',
160
+ ] as const;
171
161
 
172
162
  const STATUS_MAP: Record<
173
163
  string,
174
- { label: string; variant: 'default' | 'secondary' | 'outline' }
164
+ { variant: 'default' | 'secondary' | 'outline' }
175
165
  > = {
176
- ativo: { label: 'Ativo', variant: 'default' },
177
- rascunho: { label: 'Rascunho', variant: 'secondary' },
178
- arquivado: { label: 'Arquivado', variant: 'outline' },
179
- };
180
-
181
- const NIVEL_MAP: Record<string, string> = {
182
- iniciante: 'Iniciante',
183
- intermediario: 'Intermediario',
184
- avancado: 'Avancado',
166
+ ativo: { variant: 'default' },
167
+ rascunho: { variant: 'secondary' },
168
+ arquivado: { variant: 'outline' },
185
169
  };
186
170
 
187
171
  // ── Seed Data ────────────────────────────────────────────────────────────────
@@ -196,7 +180,7 @@ const initialCursos: Curso[] = [
196
180
  'Curso completo de React com hooks, context e patterns avancados para aplicacoes modernas',
197
181
  nivel: 'avancado',
198
182
  status: 'ativo',
199
- categorias: ['Tecnologia'],
183
+ categorias: ['technology'],
200
184
  destaque: true,
201
185
  certificado: true,
202
186
  listado: true,
@@ -212,7 +196,7 @@ const initialCursos: Curso[] = [
212
196
  'Fundamentos de design de experiencia do usuario com ferramentas modernas e pesquisa',
213
197
  nivel: 'iniciante',
214
198
  status: 'ativo',
215
- categorias: ['Design'],
199
+ categorias: ['design'],
216
200
  destaque: false,
217
201
  certificado: true,
218
202
  listado: true,
@@ -228,7 +212,7 @@ const initialCursos: Curso[] = [
228
212
  'Metodologias ageis para gestao de projetos incluindo Scrum, Kanban e SAFe em equipes',
229
213
  nivel: 'intermediario',
230
214
  status: 'ativo',
231
- categorias: ['Gestao'],
215
+ categorias: ['management'],
232
216
  destaque: true,
233
217
  certificado: true,
234
218
  listado: true,
@@ -244,7 +228,7 @@ const initialCursos: Curso[] = [
244
228
  'Estrategias de marketing digital para negocios modernos incluindo SEO, SEM e redes sociais',
245
229
  nivel: 'intermediario',
246
230
  status: 'rascunho',
247
- categorias: ['Marketing'],
231
+ categorias: ['marketing'],
248
232
  destaque: false,
249
233
  certificado: false,
250
234
  listado: false,
@@ -260,7 +244,7 @@ const initialCursos: Curso[] = [
260
244
  'Introducao a Python focado em ciencia de dados, analise estatistica e machine learning basico',
261
245
  nivel: 'intermediario',
262
246
  status: 'ativo',
263
- categorias: ['Tecnologia'],
247
+ categorias: ['technology'],
264
248
  destaque: false,
265
249
  certificado: true,
266
250
  listado: true,
@@ -276,7 +260,7 @@ const initialCursos: Curso[] = [
276
260
  'Backend com Node.js, Express e bancos de dados relacionais e NoSQL para APIs robustas',
277
261
  nivel: 'avancado',
278
262
  status: 'ativo',
279
- categorias: ['Tecnologia'],
263
+ categorias: ['technology'],
280
264
  destaque: true,
281
265
  certificado: true,
282
266
  listado: true,
@@ -292,7 +276,7 @@ const initialCursos: Curso[] = [
292
276
  'Aprenda a usar o Figma do zero para criar interfaces profissionais e prototipos interativos',
293
277
  nivel: 'iniciante',
294
278
  status: 'arquivado',
295
- categorias: ['Design'],
279
+ categorias: ['design'],
296
280
  destaque: false,
297
281
  certificado: true,
298
282
  listado: false,
@@ -308,7 +292,7 @@ const initialCursos: Curso[] = [
308
292
  'Desenvolva habilidades de lideranca e comunicacao assertiva para ambientes corporativos',
309
293
  nivel: 'iniciante',
310
294
  status: 'ativo',
311
- categorias: ['Gestao'],
295
+ categorias: ['management'],
312
296
  destaque: false,
313
297
  certificado: true,
314
298
  listado: true,
@@ -324,7 +308,7 @@ const initialCursos: Curso[] = [
324
308
  'Tecnicas avancadas de otimizacao para motores de busca, conteudo web e performance de sites',
325
309
  nivel: 'avancado',
326
310
  status: 'rascunho',
327
- categorias: ['Marketing', 'Tecnologia'],
311
+ categorias: ['marketing', 'technology'],
328
312
  destaque: false,
329
313
  certificado: false,
330
314
  listado: false,
@@ -340,7 +324,7 @@ const initialCursos: Curso[] = [
340
324
  'TypeScript aplicado em projetos reais com boas praticas, design patterns e testes',
341
325
  nivel: 'intermediario',
342
326
  status: 'ativo',
343
- categorias: ['Tecnologia'],
327
+ categorias: ['technology'],
344
328
  destaque: true,
345
329
  certificado: true,
346
330
  listado: true,
@@ -356,7 +340,7 @@ const initialCursos: Curso[] = [
356
340
  'Como criar e manter um design system escalavel para grandes equipes de produto e engenharia',
357
341
  nivel: 'avancado',
358
342
  status: 'ativo',
359
- categorias: ['Design', 'Tecnologia'],
343
+ categorias: ['design', 'technology'],
360
344
  destaque: false,
361
345
  certificado: true,
362
346
  listado: true,
@@ -372,7 +356,7 @@ const initialCursos: Curso[] = [
372
356
  'Domine Excel com formulas avancadas, dashboards profissionais e analise de dados empresariais',
373
357
  nivel: 'iniciante',
374
358
  status: 'ativo',
375
- categorias: ['Gestao', 'Financas'],
359
+ categorias: ['management', 'finance'],
376
360
  destaque: false,
377
361
  certificado: true,
378
362
  listado: true,
@@ -388,7 +372,7 @@ const initialCursos: Curso[] = [
388
372
  'Aprenda a gerenciar suas financas, investir e planejar sua aposentadoria de forma inteligente',
389
373
  nivel: 'iniciante',
390
374
  status: 'ativo',
391
- categorias: ['Financas'],
375
+ categorias: ['finance'],
392
376
  destaque: false,
393
377
  certificado: true,
394
378
  listado: true,
@@ -404,7 +388,7 @@ const initialCursos: Curso[] = [
404
388
  'Desenvolvimento de aplicativos moveis multiplataforma com Flutter e Dart do zero ao deploy',
405
389
  nivel: 'intermediario',
406
390
  status: 'ativo',
407
- categorias: ['Tecnologia'],
391
+ categorias: ['technology'],
408
392
  destaque: true,
409
393
  certificado: true,
410
394
  listado: true,
@@ -420,7 +404,7 @@ const initialCursos: Curso[] = [
420
404
  'Conceitos essenciais de direito trabalhista para gestores de RH e empreendedores',
421
405
  nivel: 'iniciante',
422
406
  status: 'rascunho',
423
- categorias: ['Direito', 'Gestao'],
407
+ categorias: ['law', 'management'],
424
408
  destaque: false,
425
409
  certificado: false,
426
410
  listado: false,
@@ -440,12 +424,13 @@ const stagger = {
440
424
 
441
425
  const fadeUp = {
442
426
  hidden: { opacity: 0, y: 16 },
443
- show: { opacity: 1, y: 0, transition: { duration: 0.35, ease: 'easeOut' } },
444
- };
427
+ show: { opacity: 1, y: 0, transition: { duration: 0.35 } },
428
+ } as const;
445
429
 
446
430
  // ── Component ────────────────────────────────────────────────────────────────
447
431
 
448
432
  export default function CursosPage() {
433
+ const t = useTranslations('lms.CoursesPage');
449
434
  const pathname = usePathname();
450
435
  const router = useRouter();
451
436
 
@@ -474,7 +459,7 @@ export default function CursosPage() {
474
459
  const [currentPage, setCurrentPage] = useState(1);
475
460
 
476
461
  const form = useForm<CursoForm>({
477
- resolver: zodResolver(cursoSchema),
462
+ resolver: zodResolver(createCursoSchema(t)),
478
463
  defaultValues: {
479
464
  codigo: '',
480
465
  nomeInterno: '',
@@ -602,7 +587,7 @@ export default function CursosPage() {
602
587
  setCursos((prev) =>
603
588
  prev.map((c) => (c.id === editingCurso.id ? { ...c, ...data } : c))
604
589
  );
605
- toast.success('Curso atualizado com sucesso!');
590
+ toast.success(t('toasts.courseUpdated'));
606
591
  setSaving(false);
607
592
  setSheetOpen(false);
608
593
  } else {
@@ -611,10 +596,10 @@ export default function CursosPage() {
611
596
  id: newId,
612
597
  ...data,
613
598
  alunosInscritos: 0,
614
- criadoEm: new Date().toISOString().split('T')[0],
599
+ criadoEm: new Date().toISOString().split('T')[0] || '',
615
600
  };
616
601
  setCursos((prev) => [newCurso, ...prev]);
617
- toast.success('Curso criado com sucesso! Redirecionando...');
602
+ toast.success(t('toasts.courseCreated'));
618
603
  setSaving(false);
619
604
  setSheetOpen(false);
620
605
  // Redirecionar para pagina dedicada do curso
@@ -632,7 +617,9 @@ export default function CursosPage() {
632
617
  next.delete(cursoToDelete.id);
633
618
  return next;
634
619
  });
635
- toast.success(`Curso "${cursoToDelete.tituloComercial}" removido.`);
620
+ toast.success(
621
+ t('toasts.courseRemoved', { title: cursoToDelete.tituloComercial })
622
+ );
636
623
  setCursoToDelete(null);
637
624
  setDeleteDialogOpen(false);
638
625
  }
@@ -641,7 +628,7 @@ export default function CursosPage() {
641
628
  function bulkDelete() {
642
629
  if (selectedIds.size === 0) return;
643
630
  setCursos((prev) => prev.filter((c) => !selectedIds.has(c.id)));
644
- toast.success(`${selectedIds.size} curso(s) removido(s).`);
631
+ toast.success(t('toasts.coursesRemoved', { count: selectedIds.size }));
645
632
  setSelectedIds(new Set());
646
633
  }
647
634
 
@@ -652,7 +639,7 @@ export default function CursosPage() {
652
639
  selectedIds.has(c.id) ? { ...c, status: 'arquivado' as const } : c
653
640
  )
654
641
  );
655
- toast.success(`${selectedIds.size} curso(s) arquivado(s).`);
642
+ toast.success(t('toasts.coursesArchived', { count: selectedIds.size }));
656
643
  setSelectedIds(new Set());
657
644
  }
658
645
 
@@ -667,21 +654,21 @@ export default function CursosPage() {
667
654
  return (
668
655
  <Page>
669
656
  <PageHeader
670
- title="Cursos"
671
- description="Gerencie seus cursos, categorias e niveis de forma facil e rapida."
657
+ title={t('title')}
658
+ description={t('description')}
672
659
  breadcrumbs={[
673
660
  {
674
- label: 'Home',
661
+ label: t('breadcrumbs.home'),
675
662
  href: '/',
676
663
  },
677
664
  {
678
- label: 'Cursos',
665
+ label: t('breadcrumbs.courses'),
679
666
  },
680
667
  ]}
681
668
  actions={
682
669
  <Button onClick={openCreateSheet} className="gap-2">
683
670
  <Plus className="size-4" />
684
- Criar Curso
671
+ {t('actions.createCourse')}
685
672
  </Button>
686
673
  }
687
674
  />
@@ -695,7 +682,7 @@ export default function CursosPage() {
695
682
  <div className="relative flex-1">
696
683
  <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
697
684
  <Input
698
- placeholder="Buscar por codigo, titulo ou nome interno..."
685
+ placeholder={t('filters.searchPlaceholder')}
699
686
  value={busca}
700
687
  onChange={(e) => setBusca(e.target.value)}
701
688
  className="pl-9"
@@ -704,30 +691,44 @@ export default function CursosPage() {
704
691
  <div className="flex flex-wrap items-center gap-2">
705
692
  <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
706
693
  <Filter className="size-3.5" />
707
- Filtros:
694
+ {t('filters.filtersLabel')}
708
695
  </div>
709
696
  <Select value={filtroStatus} onValueChange={setFiltroStatus}>
710
697
  <SelectTrigger className="h-9 w-[130px] text-xs">
711
- <SelectValue placeholder="Status" />
698
+ <SelectValue placeholder={t('status.active')} />
712
699
  </SelectTrigger>
713
700
  <SelectContent>
714
- <SelectItem value="todos">Todos Status</SelectItem>
715
- <SelectItem value="ativo">Ativo</SelectItem>
716
- <SelectItem value="rascunho">Rascunho</SelectItem>
717
- <SelectItem value="arquivado">Arquivado</SelectItem>
701
+ <SelectItem value="todos">
702
+ {t('filters.allStatuses')}
703
+ </SelectItem>
704
+ <SelectItem value="ativo">
705
+ {t('status.active')}
706
+ </SelectItem>
707
+ <SelectItem value="rascunho">
708
+ {t('status.draft')}
709
+ </SelectItem>
710
+ <SelectItem value="arquivado">
711
+ {t('status.archived')}
712
+ </SelectItem>
718
713
  </SelectContent>
719
714
  </Select>
720
715
  <Select value={filtroNivel} onValueChange={setFiltroNivel}>
721
716
  <SelectTrigger className="h-9 w-[140px] text-xs">
722
- <SelectValue placeholder="Nivel" />
717
+ <SelectValue placeholder={t('levels.beginner')} />
723
718
  </SelectTrigger>
724
719
  <SelectContent>
725
- <SelectItem value="todos">Todos Niveis</SelectItem>
726
- <SelectItem value="iniciante">Iniciante</SelectItem>
720
+ <SelectItem value="todos">
721
+ {t('filters.allLevels')}
722
+ </SelectItem>
723
+ <SelectItem value="iniciante">
724
+ {t('levels.beginner')}
725
+ </SelectItem>
727
726
  <SelectItem value="intermediario">
728
- Intermediario
727
+ {t('levels.intermediate')}
728
+ </SelectItem>
729
+ <SelectItem value="avancado">
730
+ {t('levels.advanced')}
729
731
  </SelectItem>
730
- <SelectItem value="avancado">Avancado</SelectItem>
731
732
  </SelectContent>
732
733
  </Select>
733
734
  <Select
@@ -735,13 +736,15 @@ export default function CursosPage() {
735
736
  onValueChange={setFiltroCategoria}
736
737
  >
737
738
  <SelectTrigger className="h-9 w-[140px] text-xs">
738
- <SelectValue placeholder="Categoria" />
739
+ <SelectValue placeholder={t('categories.technology')} />
739
740
  </SelectTrigger>
740
741
  <SelectContent>
741
- <SelectItem value="todos">Todas Categorias</SelectItem>
742
+ <SelectItem value="todos">
743
+ {t('filters.allCategories')}
744
+ </SelectItem>
742
745
  {CATEGORIAS.map((cat) => (
743
746
  <SelectItem key={cat} value={cat}>
744
- {cat}
747
+ {t(`categories.${cat}`)}
745
748
  </SelectItem>
746
749
  ))}
747
750
  </SelectContent>
@@ -762,7 +765,7 @@ export default function CursosPage() {
762
765
  }}
763
766
  >
764
767
  <X className="mr-1 size-3" />
765
- Limpar
768
+ {t('filters.clear')}
766
769
  </Button>
767
770
  )}
768
771
  </div>
@@ -784,7 +787,7 @@ export default function CursosPage() {
784
787
  <div className="flex items-center gap-3 rounded-lg border bg-muted/50 px-4 py-2.5">
785
788
  <CheckSquare className="size-4 text-muted-foreground" />
786
789
  <span className="text-sm font-medium">
787
- {selectedIds.size} selecionado(s)
790
+ {t('bulkActions.selected', { count: selectedIds.size })}
788
791
  </span>
789
792
  <Separator orientation="vertical" className="h-5" />
790
793
  <Button
@@ -794,7 +797,7 @@ export default function CursosPage() {
794
797
  onClick={bulkArchive}
795
798
  >
796
799
  <Archive className="size-3" />
797
- Arquivar
800
+ {t('bulkActions.archive')}
798
801
  </Button>
799
802
  <Button
800
803
  variant="outline"
@@ -803,7 +806,7 @@ export default function CursosPage() {
803
806
  onClick={bulkDelete}
804
807
  >
805
808
  <Trash2 className="size-3" />
806
- Excluir
809
+ {t('bulkActions.delete')}
807
810
  </Button>
808
811
  <Button
809
812
  variant="ghost"
@@ -811,7 +814,7 @@ export default function CursosPage() {
811
814
  className="ml-auto h-7 text-xs"
812
815
  onClick={() => setSelectedIds(new Set())}
813
816
  >
814
- Limpar selecao
817
+ {t('bulkActions.clearSelection')}
815
818
  </Button>
816
819
  </div>
817
820
  </motion.div>
@@ -852,20 +855,22 @@ export default function CursosPage() {
852
855
  <Checkbox
853
856
  checked={allPageSelected}
854
857
  onCheckedChange={toggleSelectAll}
855
- aria-label="Selecionar todos"
858
+ aria-label={t('table.selectAll')}
856
859
  />
857
860
  </TableHead>
858
- <TableHead className="w-[100px]">Codigo</TableHead>
859
- <TableHead>Titulo</TableHead>
861
+ <TableHead className="w-[100px]">
862
+ {t('table.headers.code')}
863
+ </TableHead>
864
+ <TableHead>{t('table.headers.title')}</TableHead>
860
865
  <TableHead className="hidden md:table-cell">
861
- Nivel
866
+ {t('table.headers.level')}
862
867
  </TableHead>
863
- <TableHead>Status</TableHead>
868
+ <TableHead>{t('table.headers.status')}</TableHead>
864
869
  <TableHead className="hidden lg:table-cell">
865
- Categorias
870
+ {t('table.headers.categories')}
866
871
  </TableHead>
867
872
  <TableHead className="hidden sm:table-cell text-right">
868
- Alunos
873
+ {t('table.headers.students')}
869
874
  </TableHead>
870
875
  <TableHead className="w-[50px]" />
871
876
  </TableRow>
@@ -889,7 +894,9 @@ export default function CursosPage() {
889
894
  <Checkbox
890
895
  checked={selectedIds.has(curso.id)}
891
896
  onCheckedChange={() => toggleSelect(curso.id)}
892
- aria-label={`Selecionar ${curso.tituloComercial}`}
897
+ aria-label={t('table.selectCourse', {
898
+ title: curso.tituloComercial,
899
+ })}
893
900
  />
894
901
  </TableCell>
895
902
  <TableCell>
@@ -912,12 +919,20 @@ export default function CursosPage() {
912
919
  variant="outline"
913
920
  className="text-xs font-normal"
914
921
  >
915
- {NIVEL_MAP[curso.nivel]}
922
+ {t(
923
+ `levels.${curso.nivel === 'iniciante' ? 'beginner' : curso.nivel === 'intermediario' ? 'intermediate' : 'advanced'}`
924
+ )}
916
925
  </Badge>
917
926
  </TableCell>
918
927
  <TableCell>
919
- <Badge variant={STATUS_MAP[curso.status].variant}>
920
- {STATUS_MAP[curso.status].label}
928
+ <Badge
929
+ variant={
930
+ STATUS_MAP[curso.status]?.variant || 'default'
931
+ }
932
+ >
933
+ {t(
934
+ `status.${curso.status === 'ativo' ? 'active' : curso.status === 'rascunho' ? 'draft' : 'archived'}`
935
+ )}
921
936
  </Badge>
922
937
  </TableCell>
923
938
  <TableCell className="hidden lg:table-cell">
@@ -928,7 +943,7 @@ export default function CursosPage() {
928
943
  variant="secondary"
929
944
  className="text-[10px] font-normal"
930
945
  >
931
- {cat}
946
+ {t(`categories.${cat}`)}
932
947
  </Badge>
933
948
  ))}
934
949
  </div>
@@ -945,7 +960,9 @@ export default function CursosPage() {
945
960
  className="size-8 opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100"
946
961
  >
947
962
  <MoreHorizontal className="size-4" />
948
- <span className="sr-only">Acoes</span>
963
+ <span className="sr-only">
964
+ {t('table.actions.label')}
965
+ </span>
949
966
  </Button>
950
967
  </DropdownMenuTrigger>
951
968
  <DropdownMenuContent
@@ -959,14 +976,14 @@ export default function CursosPage() {
959
976
  className="gap-2"
960
977
  >
961
978
  <Eye className="size-3.5" />
962
- Ver detalhes
979
+ {t('table.actions.viewDetails')}
963
980
  </DropdownMenuItem>
964
981
  <DropdownMenuItem
965
982
  onClick={() => openEditSheet(curso)}
966
983
  className="gap-2"
967
984
  >
968
985
  <Pencil className="size-3.5" />
969
- Editar
986
+ {t('table.actions.edit')}
970
987
  </DropdownMenuItem>
971
988
  <DropdownMenuItem
972
989
  onClick={() => {
@@ -974,13 +991,15 @@ export default function CursosPage() {
974
991
  curso.codigo
975
992
  );
976
993
  toast.info(
977
- `Codigo "${curso.codigo}" copiado.`
994
+ t('toasts.codeCopied', {
995
+ code: curso.codigo,
996
+ })
978
997
  );
979
998
  }}
980
999
  className="gap-2"
981
1000
  >
982
1001
  <Copy className="size-3.5" />
983
- Copiar codigo
1002
+ {t('table.actions.copyCode')}
984
1003
  </DropdownMenuItem>
985
1004
  <DropdownMenuSeparator />
986
1005
  <DropdownMenuItem
@@ -991,7 +1010,7 @@ export default function CursosPage() {
991
1010
  className="gap-2 text-destructive focus:text-destructive"
992
1011
  >
993
1012
  <Trash2 className="size-3.5" />
994
- Excluir
1013
+ {t('table.actions.delete')}
995
1014
  </DropdownMenuItem>
996
1015
  </DropdownMenuContent>
997
1016
  </DropdownMenu>
@@ -1005,10 +1024,10 @@ export default function CursosPage() {
1005
1024
  <div className="flex flex-col items-center gap-2">
1006
1025
  <BookOpen className="size-10 text-muted-foreground/40" />
1007
1026
  <p className="text-sm font-medium text-muted-foreground">
1008
- Nenhum curso encontrado
1027
+ {t('table.empty.title')}
1009
1028
  </p>
1010
1029
  <p className="text-xs text-muted-foreground/70">
1011
- Tente ajustar os filtros ou criar um novo curso.
1030
+ {t('table.empty.description')}
1012
1031
  </p>
1013
1032
  </div>
1014
1033
  </TableCell>
@@ -1022,22 +1041,22 @@ export default function CursosPage() {
1022
1041
  {filteredCursos.length > 0 && (
1023
1042
  <div className="flex flex-col items-center justify-between gap-3 border-t px-4 py-3 sm:flex-row">
1024
1043
  <p className="text-xs text-muted-foreground">
1025
- Mostrando{' '}
1044
+ {t('pagination.showing')}{' '}
1026
1045
  <span className="font-medium text-foreground">
1027
1046
  {(safePage - 1) * ITEMS_PER_PAGE + 1}
1028
1047
  </span>{' '}
1029
- a{' '}
1048
+ {t('pagination.to')}{' '}
1030
1049
  <span className="font-medium text-foreground">
1031
1050
  {Math.min(
1032
1051
  safePage * ITEMS_PER_PAGE,
1033
1052
  filteredCursos.length
1034
1053
  )}
1035
1054
  </span>{' '}
1036
- de{' '}
1055
+ {t('pagination.of')}{' '}
1037
1056
  <span className="font-medium text-foreground">
1038
1057
  {filteredCursos.length}
1039
1058
  </span>{' '}
1040
- resultados
1059
+ {t('pagination.results')}
1041
1060
  </p>
1042
1061
  <div className="flex items-center gap-1">
1043
1062
  <Button
@@ -1046,7 +1065,7 @@ export default function CursosPage() {
1046
1065
  className="size-8"
1047
1066
  disabled={safePage === 1}
1048
1067
  onClick={() => setCurrentPage((p) => p - 1)}
1049
- aria-label="Pagina anterior"
1068
+ aria-label={t('pagination.previousPage')}
1050
1069
  >
1051
1070
  <ChevronLeft className="size-4" />
1052
1071
  </Button>
@@ -1067,7 +1086,7 @@ export default function CursosPage() {
1067
1086
  className="size-8"
1068
1087
  disabled={safePage === totalPages}
1069
1088
  onClick={() => setCurrentPage((p) => p + 1)}
1070
- aria-label="Proxima pagina"
1089
+ aria-label={t('pagination.nextPage')}
1071
1090
  >
1072
1091
  <ChevronRight className="size-4" />
1073
1092
  </Button>
@@ -1085,28 +1104,28 @@ export default function CursosPage() {
1085
1104
  <SheetContent className="w-full overflow-y-auto sm:max-w-lg">
1086
1105
  <SheetHeader>
1087
1106
  <SheetTitle>
1088
- {editingCurso ? 'Editar Curso' : 'Criar Novo Curso'}
1107
+ {editingCurso ? t('form.title.edit') : t('form.title.create')}
1089
1108
  </SheetTitle>
1090
1109
  <SheetDescription>
1091
1110
  {editingCurso
1092
- ? 'Atualize as informacoes do curso abaixo.'
1093
- : 'Preencha os dados para criar um novo curso. Apos criar, voce sera redirecionado para a pagina do curso.'}
1111
+ ? t('form.description.edit')
1112
+ : t('form.description.create')}
1094
1113
  </SheetDescription>
1095
1114
  </SheetHeader>
1096
1115
 
1097
1116
  <form
1098
1117
  onSubmit={form.handleSubmit(onSubmit)}
1099
- className="mt-6 flex flex-col gap-5 px-1"
1118
+ className="mt-6 flex flex-col px-4 gap-5"
1100
1119
  >
1101
1120
  {/* Codigo */}
1102
1121
  <Field data-invalid={!!form.formState.errors.codigo}>
1103
1122
  <FieldLabel htmlFor="codigo">
1104
1123
  <Hash className="size-3.5 text-muted-foreground" />
1105
- Codigo do Curso
1124
+ {t('form.fields.code.label')}
1106
1125
  </FieldLabel>
1107
1126
  <Input
1108
1127
  id="codigo"
1109
- placeholder="Ex: REACT-ADV"
1128
+ placeholder={t('form.fields.code.placeholder')}
1110
1129
  className="uppercase"
1111
1130
  {...form.register('codigo')}
1112
1131
  />
@@ -1114,16 +1133,18 @@ export default function CursosPage() {
1114
1133
  <FieldError>{form.formState.errors.codigo.message}</FieldError>
1115
1134
  )}
1116
1135
  <FieldDescription>
1117
- Identificador unico do curso (ex: REACT-ADV)
1136
+ {t('form.fields.code.description')}
1118
1137
  </FieldDescription>
1119
1138
  </Field>
1120
1139
 
1121
1140
  {/* Nome Interno */}
1122
1141
  <Field data-invalid={!!form.formState.errors.nomeInterno}>
1123
- <FieldLabel htmlFor="nomeInterno">Nome Interno</FieldLabel>
1142
+ <FieldLabel htmlFor="nomeInterno">
1143
+ {t('form.fields.internalName.label')}
1144
+ </FieldLabel>
1124
1145
  <Input
1125
1146
  id="nomeInterno"
1126
- placeholder="Ex: react-avancado"
1147
+ placeholder={t('form.fields.internalName.placeholder')}
1127
1148
  {...form.register('nomeInterno')}
1128
1149
  />
1129
1150
  {form.formState.errors.nomeInterno && (
@@ -1132,18 +1153,18 @@ export default function CursosPage() {
1132
1153
  </FieldError>
1133
1154
  )}
1134
1155
  <FieldDescription>
1135
- Slug interno para uso do sistema
1156
+ {t('form.fields.internalName.description')}
1136
1157
  </FieldDescription>
1137
1158
  </Field>
1138
1159
 
1139
1160
  {/* Titulo Comercial */}
1140
1161
  <Field data-invalid={!!form.formState.errors.tituloComercial}>
1141
1162
  <FieldLabel htmlFor="tituloComercial">
1142
- Titulo Comercial
1163
+ {t('form.fields.commercialTitle.label')}
1143
1164
  </FieldLabel>
1144
1165
  <Input
1145
1166
  id="tituloComercial"
1146
- placeholder="Ex: React Avancado"
1167
+ placeholder={t('form.fields.commercialTitle.placeholder')}
1147
1168
  {...form.register('tituloComercial')}
1148
1169
  />
1149
1170
  {form.formState.errors.tituloComercial && (
@@ -1155,10 +1176,12 @@ export default function CursosPage() {
1155
1176
 
1156
1177
  {/* Descricao */}
1157
1178
  <Field data-invalid={!!form.formState.errors.descricao}>
1158
- <FieldLabel htmlFor="descricao">Descricao</FieldLabel>
1179
+ <FieldLabel htmlFor="descricao">
1180
+ {t('form.fields.description.label')}
1181
+ </FieldLabel>
1159
1182
  <Textarea
1160
1183
  id="descricao"
1161
- placeholder="Descreva o conteudo e objetivos do curso..."
1184
+ placeholder={t('form.fields.description.placeholder')}
1162
1185
  rows={4}
1163
1186
  {...form.register('descricao')}
1164
1187
  />
@@ -1172,21 +1195,27 @@ export default function CursosPage() {
1172
1195
  {/* Nivel + Status side by side */}
1173
1196
  <div className="grid grid-cols-2 gap-4">
1174
1197
  <Field data-invalid={!!form.formState.errors.nivel}>
1175
- <FieldLabel>Nivel</FieldLabel>
1198
+ <FieldLabel>{t('form.fields.level.label')}</FieldLabel>
1176
1199
  <Controller
1177
1200
  control={form.control}
1178
1201
  name="nivel"
1179
1202
  render={({ field }) => (
1180
1203
  <Select value={field.value} onValueChange={field.onChange}>
1181
1204
  <SelectTrigger>
1182
- <SelectValue placeholder="Selecione" />
1205
+ <SelectValue
1206
+ placeholder={t('form.fields.level.placeholder')}
1207
+ />
1183
1208
  </SelectTrigger>
1184
1209
  <SelectContent>
1185
- {NIVEIS.map((n) => (
1186
- <SelectItem key={n.value} value={n.value}>
1187
- {n.label}
1188
- </SelectItem>
1189
- ))}
1210
+ <SelectItem value="iniciante">
1211
+ {t('levels.beginner')}
1212
+ </SelectItem>
1213
+ <SelectItem value="intermediario">
1214
+ {t('levels.intermediate')}
1215
+ </SelectItem>
1216
+ <SelectItem value="avancado">
1217
+ {t('levels.advanced')}
1218
+ </SelectItem>
1190
1219
  </SelectContent>
1191
1220
  </Select>
1192
1221
  )}
@@ -1197,21 +1226,27 @@ export default function CursosPage() {
1197
1226
  </Field>
1198
1227
 
1199
1228
  <Field data-invalid={!!form.formState.errors.status}>
1200
- <FieldLabel>Status</FieldLabel>
1229
+ <FieldLabel>{t('form.fields.status.label')}</FieldLabel>
1201
1230
  <Controller
1202
1231
  control={form.control}
1203
1232
  name="status"
1204
1233
  render={({ field }) => (
1205
1234
  <Select value={field.value} onValueChange={field.onChange}>
1206
1235
  <SelectTrigger>
1207
- <SelectValue placeholder="Selecione" />
1236
+ <SelectValue
1237
+ placeholder={t('form.fields.status.placeholder')}
1238
+ />
1208
1239
  </SelectTrigger>
1209
1240
  <SelectContent>
1210
- {STATUS_OPTIONS.map((s) => (
1211
- <SelectItem key={s.value} value={s.value}>
1212
- {s.label}
1213
- </SelectItem>
1214
- ))}
1241
+ <SelectItem value="ativo">
1242
+ {t('status.active')}
1243
+ </SelectItem>
1244
+ <SelectItem value="rascunho">
1245
+ {t('status.draft')}
1246
+ </SelectItem>
1247
+ <SelectItem value="arquivado">
1248
+ {t('status.archived')}
1249
+ </SelectItem>
1215
1250
  </SelectContent>
1216
1251
  </Select>
1217
1252
  )}
@@ -1228,9 +1263,9 @@ export default function CursosPage() {
1228
1263
 
1229
1264
  {/* Categorias - Multi select via checkboxes */}
1230
1265
  <Field data-invalid={!!form.formState.errors.categorias}>
1231
- <FieldLabel>Categorias</FieldLabel>
1266
+ <FieldLabel>{t('form.fields.categories.label')}</FieldLabel>
1232
1267
  <FieldDescription>
1233
- Selecione uma ou mais categorias
1268
+ {t('form.fields.categories.description')}
1234
1269
  </FieldDescription>
1235
1270
  <Controller
1236
1271
  control={form.control}
@@ -1256,7 +1291,7 @@ export default function CursosPage() {
1256
1291
  }
1257
1292
  }}
1258
1293
  />
1259
- {cat}
1294
+ {t(`categories.${cat}`)}
1260
1295
  </label>
1261
1296
  );
1262
1297
  })}
@@ -1274,16 +1309,18 @@ export default function CursosPage() {
1274
1309
 
1275
1310
  {/* Flags */}
1276
1311
  <div className="flex flex-col gap-4">
1277
- <p className="text-sm font-medium">Flags Principais</p>
1312
+ <p className="text-sm font-medium">{t('form.flags.title')}</p>
1278
1313
  <Controller
1279
1314
  control={form.control}
1280
1315
  name="destaque"
1281
1316
  render={({ field }) => (
1282
1317
  <div className="flex items-center justify-between rounded-md border px-4 py-3">
1283
1318
  <div className="flex flex-col gap-0.5">
1284
- <span className="text-sm font-medium">Destaque</span>
1319
+ <span className="text-sm font-medium">
1320
+ {t('form.flags.featured.label')}
1321
+ </span>
1285
1322
  <span className="text-xs text-muted-foreground">
1286
- Exibir como curso em destaque na vitrine
1323
+ {t('form.flags.featured.description')}
1287
1324
  </span>
1288
1325
  </div>
1289
1326
  <Switch
@@ -1299,9 +1336,11 @@ export default function CursosPage() {
1299
1336
  render={({ field }) => (
1300
1337
  <div className="flex items-center justify-between rounded-md border px-4 py-3">
1301
1338
  <div className="flex flex-col gap-0.5">
1302
- <span className="text-sm font-medium">Certificado</span>
1339
+ <span className="text-sm font-medium">
1340
+ {t('form.flags.certificate.label')}
1341
+ </span>
1303
1342
  <span className="text-xs text-muted-foreground">
1304
- Emitir certificado ao concluir o curso
1343
+ {t('form.flags.certificate.description')}
1305
1344
  </span>
1306
1345
  </div>
1307
1346
  <Switch
@@ -1317,9 +1356,11 @@ export default function CursosPage() {
1317
1356
  render={({ field }) => (
1318
1357
  <div className="flex items-center justify-between rounded-md border px-4 py-3">
1319
1358
  <div className="flex flex-col gap-0.5">
1320
- <span className="text-sm font-medium">Listado</span>
1359
+ <span className="text-sm font-medium">
1360
+ {t('form.flags.listed.label')}
1361
+ </span>
1321
1362
  <span className="text-xs text-muted-foreground">
1322
- Visivel no catalogo publico de cursos
1363
+ {t('form.flags.listed.description')}
1323
1364
  </span>
1324
1365
  </div>
1325
1366
  <Switch
@@ -1332,16 +1373,11 @@ export default function CursosPage() {
1332
1373
  </div>
1333
1374
 
1334
1375
  <SheetFooter className="mt-2 gap-2 pb-6">
1335
- <Button
1336
- type="button"
1337
- variant="outline"
1338
- onClick={() => setSheetOpen(false)}
1339
- >
1340
- Cancelar
1341
- </Button>
1342
1376
  <Button type="submit" disabled={saving} className="gap-2">
1343
1377
  {saving && <Loader2 className="size-4 animate-spin" />}
1344
- {editingCurso ? 'Salvar Alteracoes' : 'Criar Curso'}
1378
+ {editingCurso
1379
+ ? t('form.actions.save')
1380
+ : t('form.actions.create')}
1345
1381
  </Button>
1346
1382
  </SheetFooter>
1347
1383
  </form>
@@ -1354,10 +1390,10 @@ export default function CursosPage() {
1354
1390
  <DialogHeader>
1355
1391
  <DialogTitle className="flex items-center gap-2">
1356
1392
  <AlertTriangle className="size-5 text-destructive" />
1357
- Confirmar Exclusao
1393
+ {t('deleteDialog.title')}
1358
1394
  </DialogTitle>
1359
1395
  <DialogDescription>
1360
- Tem certeza que deseja excluir o curso{' '}
1396
+ {t('deleteDialog.description')}{' '}
1361
1397
  <strong className="text-foreground">
1362
1398
  {cursoToDelete?.tituloComercial}
1363
1399
  </strong>
@@ -1365,8 +1401,9 @@ export default function CursosPage() {
1365
1401
  {cursoToDelete && cursoToDelete.alunosInscritos > 0 && (
1366
1402
  <span className="mt-2 flex items-center gap-1.5 rounded-md bg-amber-50 px-3 py-2 text-xs font-medium text-amber-700">
1367
1403
  <AlertTriangle className="size-3.5" />
1368
- Este curso possui {cursoToDelete.alunosInscritos} aluno(s)
1369
- matriculado(s). A exclusao e irreversivel.
1404
+ {t('deleteDialog.warning', {
1405
+ count: cursoToDelete.alunosInscritos,
1406
+ })}
1370
1407
  </span>
1371
1408
  )}
1372
1409
  </DialogDescription>
@@ -1376,7 +1413,7 @@ export default function CursosPage() {
1376
1413
  variant="outline"
1377
1414
  onClick={() => setDeleteDialogOpen(false)}
1378
1415
  >
1379
- Cancelar
1416
+ {t('deleteDialog.actions.cancel')}
1380
1417
  </Button>
1381
1418
  <Button
1382
1419
  variant="destructive"
@@ -1384,7 +1421,7 @@ export default function CursosPage() {
1384
1421
  className="gap-2"
1385
1422
  >
1386
1423
  <Trash2 className="size-4" />
1387
- Excluir Curso
1424
+ {t('deleteDialog.actions.delete')}
1388
1425
  </Button>
1389
1426
  </DialogFooter>
1390
1427
  </DialogContent>