@hed-hog/lms 0.0.306 → 0.0.309

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 (117) hide show
  1. package/dist/course/course-structure.controller.d.ts +60 -0
  2. package/dist/course/course-structure.controller.d.ts.map +1 -1
  3. package/dist/course/course-structure.controller.js +79 -0
  4. package/dist/course/course-structure.controller.js.map +1 -1
  5. package/dist/course/course-structure.service.d.ts +61 -1
  6. package/dist/course/course-structure.service.d.ts.map +1 -1
  7. package/dist/course/course-structure.service.js +326 -1
  8. package/dist/course/course-structure.service.js.map +1 -1
  9. package/dist/course/course.controller.d.ts +52 -4
  10. package/dist/course/course.controller.d.ts.map +1 -1
  11. package/dist/course/course.service.d.ts +52 -5
  12. package/dist/course/course.service.d.ts.map +1 -1
  13. package/dist/course/course.service.js +78 -57
  14. package/dist/course/course.service.js.map +1 -1
  15. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  16. package/dist/course/dto/create-course-structure-lesson.dto.js +5 -1
  17. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  18. package/dist/course/dto/create-course.dto.d.ts +1 -1
  19. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  20. package/dist/course/dto/create-course.dto.js +4 -1
  21. package/dist/course/dto/create-course.dto.js.map +1 -1
  22. package/dist/course/dto/move-lesson.dto.d.ts +10 -0
  23. package/dist/course/dto/move-lesson.dto.d.ts.map +1 -0
  24. package/dist/course/dto/move-lesson.dto.js +28 -0
  25. package/dist/course/dto/move-lesson.dto.js.map +1 -0
  26. package/dist/course/dto/paste-lessons.dto.d.ts +4 -0
  27. package/dist/course/dto/paste-lessons.dto.d.ts.map +1 -0
  28. package/dist/course/dto/paste-lessons.dto.js +24 -0
  29. package/dist/course/dto/paste-lessons.dto.js.map +1 -0
  30. package/dist/course/dto/reorder-lessons.dto.d.ts +5 -0
  31. package/dist/course/dto/reorder-lessons.dto.d.ts.map +1 -0
  32. package/dist/course/dto/reorder-lessons.dto.js +24 -0
  33. package/dist/course/dto/reorder-lessons.dto.js.map +1 -0
  34. package/dist/course/dto/reorder-sessions.dto.d.ts +5 -0
  35. package/dist/course/dto/reorder-sessions.dto.d.ts.map +1 -0
  36. package/dist/course/dto/reorder-sessions.dto.js +24 -0
  37. package/dist/course/dto/reorder-sessions.dto.js.map +1 -0
  38. package/dist/training/training.controller.js +1 -1
  39. package/dist/training/training.controller.js.map +1 -1
  40. package/hedhog/data/image_type.yaml +20 -0
  41. package/hedhog/data/menu.yaml +2 -2
  42. package/hedhog/data/route.yaml +60 -6
  43. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +146 -165
  44. package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +70 -0
  45. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +372 -22
  46. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +437 -77
  47. package/hedhog/frontend/app/classes/page.tsx.ejs +311 -289
  48. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +10 -7
  49. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +23 -32
  50. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +3 -9
  51. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +26 -16
  52. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +19 -5
  53. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +10 -14
  54. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +131 -107
  55. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +10 -7
  56. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +38 -19
  57. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +1 -1
  58. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
  59. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +336 -1057
  60. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +45 -0
  61. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -0
  62. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -0
  63. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +64 -0
  64. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
  65. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
  66. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
  67. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -0
  68. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
  69. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -0
  70. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +52 -0
  71. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -0
  72. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -0
  73. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1827 -0
  74. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -0
  75. package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +41 -0
  76. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +184 -0
  77. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -0
  78. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +96 -0
  79. package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +74 -0
  80. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -0
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +948 -0
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -0
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +150 -0
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +182 -0
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +52 -0
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -0
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -0
  89. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -0
  90. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +122 -0
  91. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -0
  92. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +97 -0
  93. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +347 -0
  94. package/hedhog/frontend/app/courses/[id]/structure/_data/course-structure-contract.ts.ejs +195 -0
  95. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +420 -0
  96. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +254 -0
  97. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +987 -0
  98. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +86 -0
  99. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure.ts.ejs +160 -0
  100. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -3212
  101. package/hedhog/frontend/app/courses/page.tsx.ejs +45 -26
  102. package/hedhog/frontend/app/{training → paths}/page.tsx.ejs +29 -7
  103. package/hedhog/frontend/messages/en.json +88 -10
  104. package/hedhog/frontend/messages/pt.json +88 -10
  105. package/hedhog/table/course.yaml +1 -1
  106. package/hedhog/table/image_type.yaml +14 -0
  107. package/package.json +7 -7
  108. package/src/course/course-structure.controller.ts +63 -0
  109. package/src/course/course-structure.service.ts +390 -3
  110. package/src/course/course.service.ts +59 -27
  111. package/src/course/dto/create-course-structure-lesson.dto.ts +3 -2
  112. package/src/course/dto/create-course.dto.ts +4 -1
  113. package/src/course/dto/move-lesson.dto.ts +17 -0
  114. package/src/course/dto/paste-lessons.dto.ts +9 -0
  115. package/src/course/dto/reorder-lessons.dto.ts +10 -0
  116. package/src/course/dto/reorder-sessions.dto.ts +10 -0
  117. package/src/training/training.controller.ts +1 -1
@@ -1,15 +1,8 @@
1
1
  'use client';
2
2
 
3
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
3
4
  import { Button } from '@/components/ui/button';
4
5
  import { Calendar } from '@/components/ui/calendar';
5
- import {
6
- Command,
7
- CommandEmpty,
8
- CommandGroup,
9
- CommandInput,
10
- CommandItem,
11
- CommandList,
12
- } from '@/components/ui/command';
13
6
  import {
14
7
  Dialog,
15
8
  DialogContent,
@@ -49,7 +42,7 @@ import {
49
42
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
50
43
  import { zodResolver } from '@hookform/resolvers/zod';
51
44
  import { format } from 'date-fns';
52
- import { CalendarIcon, ChevronsUpDown, Loader2, Plus } from 'lucide-react';
45
+ import { CalendarIcon, Loader2, Plus } from 'lucide-react';
53
46
  import { useTranslations } from 'next-intl';
54
47
  import { useEffect, useMemo, useRef, useState } from 'react';
55
48
  import type { DateRange } from 'react-day-picker';
@@ -140,6 +133,7 @@ type InstructorOption = {
140
133
  id: number;
141
134
  name: string;
142
135
  personId?: number;
136
+ avatarId?: number | null;
143
137
  qualificationSlugs?: string[];
144
138
  };
145
139
 
@@ -153,6 +147,8 @@ type InstructorApiRow = {
153
147
  label?: string;
154
148
  personId?: number | string;
155
149
  person_id?: number | string;
150
+ avatarId?: number | string | null;
151
+ avatar_id?: number | string | null;
156
152
  qualificationSlugs?: string[];
157
153
  };
158
154
 
@@ -306,10 +302,16 @@ function normalizeInstructorOption(
306
302
  item?.name ?? item?.nome ?? item?.full_name ?? item?.label ?? ''
307
303
  ).trim();
308
304
  if (!id || !name) return null;
305
+ const rawAvatarId = item?.avatarId ?? item?.avatar_id;
306
+ const avatarId =
307
+ rawAvatarId !== undefined && rawAvatarId !== null
308
+ ? Number(rawAvatarId) || null
309
+ : null;
309
310
  return {
310
311
  id,
311
312
  name,
312
313
  personId: Number(item?.personId ?? item?.person_id ?? 0) || undefined,
314
+ avatarId,
313
315
  qualificationSlugs: Array.isArray(item?.qualificationSlugs)
314
316
  ? item.qualificationSlugs
315
317
  : undefined,
@@ -516,8 +518,6 @@ export function ClassFormSheet({
516
518
  const [savingCourse, setSavingCourse] = useState(false);
517
519
  const [dateRangeOpen, setDateRangeOpen] = useState(false);
518
520
  const [dateRangeDraft, setDateRangeDraft] = useState<DateRange | undefined>();
519
- const [professorOpen, setProfessorOpen] = useState(false);
520
- const [professorSearch, setProfessorSearch] = useState('');
521
521
  const [createProfessorDialogOpen, setCreateProfessorDialogOpen] =
522
522
  useState(false);
523
523
  const [courseSheetOpen, setCourseSheetOpen] = useState(false);
@@ -561,56 +561,6 @@ export function ClassFormSheet({
561
561
 
562
562
  // ── Queries ────────────────────────────────────────────────────────────────
563
563
 
564
- const {
565
- data: professorOptions = [],
566
- isFetching: loadingProfessores,
567
- refetch: refetchProfessorOptions,
568
- } = useQuery<InstructorOption[]>({
569
- queryKey: ['class-form-sheet-professors', professorSearch],
570
- queryFn: async () => {
571
- const response = await request<
572
- | InstructorApiRow[]
573
- | {
574
- data?: InstructorApiRow[];
575
- items?: InstructorApiRow[];
576
- rows?: InstructorApiRow[];
577
- }
578
- >({
579
- url: '/lms/instructors',
580
- method: 'GET',
581
- params: {
582
- page: 1,
583
- pageSize: 100,
584
- qualificationSlugs: ['class-sessions'],
585
- ...(professorSearch.trim() ? { search: professorSearch.trim() } : {}),
586
- },
587
- });
588
-
589
- const payload = response.data;
590
- const rows = Array.isArray(payload)
591
- ? payload
592
- : Array.isArray(payload?.data)
593
- ? payload.data
594
- : Array.isArray(payload?.items)
595
- ? payload.items
596
- : Array.isArray(payload?.rows)
597
- ? payload.rows
598
- : [];
599
-
600
- const unique = new Map<number, InstructorOption>();
601
- for (const row of rows) {
602
- const normalized = normalizeInstructorOption(row);
603
- if (!normalized) continue;
604
- unique.set(normalized.id, normalized);
605
- }
606
-
607
- return Array.from(unique.values()).sort((a, b) =>
608
- a.name.localeCompare(b.name)
609
- );
610
- },
611
- initialData: [],
612
- });
613
-
614
564
  const { data: categoryListData, refetch: refetchCategoryOptions } =
615
565
  useQuery<ApiCategoryList>({
616
566
  queryKey: ['class-form-sheet-categories'],
@@ -736,10 +686,6 @@ export function ClassFormSheet({
736
686
  // eslint-disable-next-line react-hooks/exhaustive-deps
737
687
  }, [open, classId]);
738
688
 
739
- useEffect(() => {
740
- if (professorOpen) void refetchProfessorOptions();
741
- }, [professorOpen, refetchProfessorOptions]);
742
-
743
689
  useEffect(() => {
744
690
  if (courseSheetOpen) void refetchCategoryOptions();
745
691
  }, [courseSheetOpen, refetchCategoryOptions]);
@@ -1672,106 +1618,141 @@ export function ClassFormSheet({
1672
1618
  {t('form.fields.professor.label')}{' '}
1673
1619
  <span className="text-destructive">*</span>
1674
1620
  </FieldLabel>
1675
- <Controller
1676
- name="professor"
1677
- control={form.control}
1678
- render={({ field }) => (
1679
- <div className="flex items-end gap-2">
1680
- <div className="flex-1">
1681
- <Popover
1682
- open={professorOpen}
1683
- onOpenChange={setProfessorOpen}
1684
- >
1685
- <PopoverTrigger asChild>
1686
- <Button
1687
- type="button"
1688
- variant="outline"
1689
- role="combobox"
1690
- className="w-full justify-between"
1691
- >
1692
- <span className="truncate text-left">
1693
- {field.value ||
1694
- t('form.fields.professor.placeholder')}
1695
- </span>
1696
- {loadingProfessores ? (
1697
- <Loader2 className="ml-2 h-4 w-4 shrink-0 animate-spin opacity-60" />
1698
- ) : (
1699
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
1700
- )}
1701
- </Button>
1702
- </PopoverTrigger>
1703
- <PopoverContent className="p-0" align="start">
1704
- <Command shouldFilter={false}>
1705
- <CommandInput
1706
- placeholder={t(
1707
- 'form.fields.professor.placeholder'
1708
- )}
1709
- value={professorSearch}
1710
- onValueChange={setProfessorSearch}
1711
- />
1712
- <CommandList>
1713
- <CommandEmpty>
1714
- <div className="flex flex-col items-center gap-3 px-2 py-4">
1715
- <p className="text-sm text-muted-foreground">
1716
- Nenhum professor encontrado.
1717
- </p>
1718
- <Button
1719
- type="button"
1720
- variant="outline"
1721
- size="sm"
1722
- className="w-full"
1723
- onClick={() => {
1724
- setProfessorOpen(false);
1725
- setCreateProfessorDialogOpen(true);
1726
- }}
1727
- >
1728
- <Plus className="mr-2 h-4 w-4" />
1729
- Cadastrar novo professor
1730
- </Button>
1731
- </div>
1732
- </CommandEmpty>
1733
- <CommandGroup>
1734
- {professorOptions.map((professor) => (
1735
- <CommandItem
1736
- key={professor.id}
1737
- value={`${professor.name}-${professor.id}`}
1738
- onSelect={() => {
1739
- form.setValue(
1740
- 'instructorId',
1741
- professor.id,
1742
- {
1743
- shouldDirty: true,
1744
- shouldTouch: true,
1745
- shouldValidate: true,
1746
- }
1747
- );
1748
- field.onChange(professor.name);
1749
- setProfessorOpen(false);
1750
- setProfessorSearch('');
1751
- }}
1752
- >
1753
- {professor.name}
1754
- </CommandItem>
1755
- ))}
1756
- </CommandGroup>
1757
- </CommandList>
1758
- </Command>
1759
- </PopoverContent>
1760
- </Popover>
1761
- </div>
1762
- <Button
1763
- type="button"
1764
- variant="outline"
1765
- size="icon"
1766
- className="shrink-0"
1767
- onClick={() => setCreateProfessorDialogOpen(true)}
1768
- aria-label="Cadastrar novo professor"
1769
- >
1770
- <Plus className="h-4 w-4" />
1771
- </Button>
1772
- </div>
1773
- )}
1774
- />
1621
+ <div className="flex items-end gap-2">
1622
+ <div className="flex-1">
1623
+ <EntityPicker<InstructorOption, TurmaForm>
1624
+ form={form}
1625
+ name="instructorId"
1626
+ valueType="number"
1627
+ placeholder={t('form.fields.professor.placeholder')}
1628
+ initialSelectedLabel={watchedFormValues.professor ?? ''}
1629
+ searchPlaceholder={t('form.fields.professor.placeholder')}
1630
+ emptyStateDescription="Nenhum professor encontrado."
1631
+ noResultsLabel="Nenhum professor encontrado."
1632
+ showCreateButton={false}
1633
+ clearable={false}
1634
+ getOptionValue={(opt) => opt.id}
1635
+ getOptionLabel={(opt) => opt.name}
1636
+ onChange={(value, option) => {
1637
+ form.setValue(
1638
+ 'instructorId',
1639
+ value as number | undefined,
1640
+ {
1641
+ shouldDirty: true,
1642
+ shouldTouch: true,
1643
+ shouldValidate: true,
1644
+ }
1645
+ );
1646
+ form.setValue('professor', option?.name ?? '', {
1647
+ shouldDirty: true,
1648
+ shouldTouch: true,
1649
+ shouldValidate: true,
1650
+ });
1651
+ }}
1652
+ loadOptions={async ({ page, pageSize, search }) => {
1653
+ const response = await request<
1654
+ | InstructorApiRow[]
1655
+ | {
1656
+ data?: InstructorApiRow[];
1657
+ items?: InstructorApiRow[];
1658
+ rows?: InstructorApiRow[];
1659
+ total?: number;
1660
+ lastPage?: number;
1661
+ }
1662
+ >({
1663
+ url: '/lms/instructors',
1664
+ method: 'GET',
1665
+ params: {
1666
+ page,
1667
+ pageSize,
1668
+ qualificationSlugs: ['class-sessions'],
1669
+ ...(search.trim() ? { search: search.trim() } : {}),
1670
+ },
1671
+ });
1672
+
1673
+ const payload = response.data;
1674
+ const rows = Array.isArray(payload)
1675
+ ? payload
1676
+ : Array.isArray(payload?.data)
1677
+ ? payload.data
1678
+ : Array.isArray(payload?.items)
1679
+ ? payload.items
1680
+ : Array.isArray(payload?.rows)
1681
+ ? payload.rows
1682
+ : [];
1683
+ const lastPage =
1684
+ !Array.isArray(payload) && payload?.lastPage
1685
+ ? payload.lastPage
1686
+ : 1;
1687
+
1688
+ const items = rows
1689
+ .map(normalizeInstructorOption)
1690
+ .filter(
1691
+ (opt): opt is InstructorOption => opt !== null
1692
+ );
1693
+
1694
+ return { items, hasMore: page < lastPage };
1695
+ }}
1696
+ renderOption={({ option }) => {
1697
+ const initials = option.name
1698
+ .split(' ')
1699
+ .filter(Boolean)
1700
+ .slice(0, 2)
1701
+ .map((p) => p[0]?.toUpperCase() ?? '')
1702
+ .join('');
1703
+ const avatarUrl = option.avatarId
1704
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${option.avatarId}`
1705
+ : undefined;
1706
+ return (
1707
+ <div className="flex min-w-0 items-center gap-3 py-0.5">
1708
+ <Avatar className="h-8 w-8 shrink-0 rounded-lg border border-border/60">
1709
+ <AvatarImage src={avatarUrl} />
1710
+ <AvatarFallback className="rounded-lg bg-muted text-[11px] font-semibold text-foreground">
1711
+ {initials}
1712
+ </AvatarFallback>
1713
+ </Avatar>
1714
+ <span className="truncate text-sm">
1715
+ {option.name}
1716
+ </span>
1717
+ </div>
1718
+ );
1719
+ }}
1720
+ renderSelectedValue={({ option, label }) => {
1721
+ const name = option?.name ?? label;
1722
+ const initials = name
1723
+ .split(' ')
1724
+ .filter(Boolean)
1725
+ .slice(0, 2)
1726
+ .map((p) => p[0]?.toUpperCase() ?? '')
1727
+ .join('');
1728
+ const avatarUrl = option?.avatarId
1729
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${option.avatarId}`
1730
+ : undefined;
1731
+ return (
1732
+ <div className="flex items-center gap-2">
1733
+ <Avatar className="h-5 w-5 shrink-0 rounded">
1734
+ <AvatarImage src={avatarUrl} />
1735
+ <AvatarFallback className="rounded bg-muted text-[10px] font-semibold">
1736
+ {initials}
1737
+ </AvatarFallback>
1738
+ </Avatar>
1739
+ <span className="truncate">{name}</span>
1740
+ </div>
1741
+ );
1742
+ }}
1743
+ />
1744
+ </div>
1745
+ <Button
1746
+ type="button"
1747
+ variant="outline"
1748
+ size="icon"
1749
+ className="shrink-0"
1750
+ onClick={() => setCreateProfessorDialogOpen(true)}
1751
+ aria-label="Cadastrar novo professor"
1752
+ >
1753
+ <Plus className="h-4 w-4" />
1754
+ </Button>
1755
+ </div>
1775
1756
  <FieldError>
1776
1757
  {form.formState.errors.professor?.message}
1777
1758
  </FieldError>
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils';
4
+ import { useApp } from '@hed-hog/next-app-provider';
5
+ import { BookOpen } from 'lucide-react';
6
+ import { useEffect, useState } from 'react';
7
+
8
+ type CourseAvatarProps = {
9
+ fileId?: number | null;
10
+ title: string;
11
+ className?: string;
12
+ iconSize?: string;
13
+ };
14
+
15
+ export function CourseAvatar({
16
+ fileId,
17
+ title,
18
+ className,
19
+ iconSize = 'size-6',
20
+ }: CourseAvatarProps) {
21
+ const { request } = useApp();
22
+ const [url, setUrl] = useState<string | null>(null);
23
+
24
+ useEffect(() => {
25
+ if (!fileId) {
26
+ setUrl(null);
27
+ return;
28
+ }
29
+
30
+ let cancelled = false;
31
+
32
+ request<{ url?: string }>({
33
+ url: `/file/open/${fileId}`,
34
+ method: 'PUT',
35
+ })
36
+ .then((res) => {
37
+ if (!cancelled && res?.data?.url) {
38
+ setUrl(res.data.url);
39
+ }
40
+ })
41
+ .catch(() => {
42
+ if (!cancelled) setUrl(null);
43
+ });
44
+
45
+ return () => {
46
+ cancelled = true;
47
+ };
48
+ }, [fileId, request]);
49
+
50
+ if (url) {
51
+ return (
52
+ <img
53
+ src={url}
54
+ alt={title}
55
+ className={cn('shrink-0 object-cover', className)}
56
+ />
57
+ );
58
+ }
59
+
60
+ return (
61
+ <div
62
+ className={cn(
63
+ 'flex shrink-0 items-center justify-center rounded-xl border bg-muted/60',
64
+ className
65
+ )}
66
+ >
67
+ <BookOpen className={cn('text-foreground', iconSize)} />
68
+ </div>
69
+ );
70
+ }