@hed-hog/lms 0.0.279 → 0.0.285
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/README.md +2 -0
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +28 -12
- package/hedhog/frontend/app/classes/page.tsx.ejs +28 -99
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +152 -109
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +20 -32
- package/hedhog/frontend/app/courses/page.tsx.ejs +31 -106
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +43 -17
- package/hedhog/frontend/app/exams/page.tsx.ejs +27 -98
- package/hedhog/frontend/app/training/page.tsx.ejs +29 -99
- package/hedhog/frontend/messages/en.json +14 -4
- package/hedhog/frontend/messages/pt.json +14 -4
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
```markdown
|
|
1
2
|
# @hed-hog/lms
|
|
2
3
|
|
|
3
4
|
## 1. Visão geral do módulo
|
|
@@ -676,3 +677,4 @@ const certificado = await prisma.certificate.create({
|
|
|
676
677
|
---
|
|
677
678
|
|
|
678
679
|
Para mais detalhes sobre integração e uso, consulte a documentação geral do HedHog e os serviços que consomem o módulo `@hed-hog/lms`.
|
|
680
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Page, PageHeader } from '@/components/entity-list';
|
|
3
|
+
import { EmptyState, Page, PageHeader } from '@/components/entity-list';
|
|
4
4
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
5
5
|
import { Badge } from '@/components/ui/badge';
|
|
6
6
|
import { Button } from '@/components/ui/button';
|
|
@@ -851,16 +851,32 @@ export default function TurmaDetalhePage() {
|
|
|
851
851
|
))}
|
|
852
852
|
</div>
|
|
853
853
|
) : filteredAlunos.length === 0 ? (
|
|
854
|
-
<
|
|
855
|
-
<
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
854
|
+
<EmptyState
|
|
855
|
+
icon={<Users className="h-12 w-12" />}
|
|
856
|
+
title={
|
|
857
|
+
alunoSearch
|
|
858
|
+
? t('students.empty.notFound')
|
|
859
|
+
: t('students.empty.notEnrolled')
|
|
860
|
+
}
|
|
861
|
+
description={
|
|
862
|
+
alunoSearch
|
|
863
|
+
? t('students.empty.notFoundDescription')
|
|
864
|
+
: t('students.empty.notEnrolledDescription')
|
|
865
|
+
}
|
|
866
|
+
actionLabel={
|
|
867
|
+
alunoSearch
|
|
868
|
+
? t('students.empty.clearSearch')
|
|
869
|
+
: t('students.actions.addStudent')
|
|
870
|
+
}
|
|
871
|
+
onAction={() => {
|
|
872
|
+
if (alunoSearch) {
|
|
873
|
+
setAlunoSearch('');
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
setAddAlunoDialogOpen(true);
|
|
878
|
+
}}
|
|
879
|
+
/>
|
|
864
880
|
) : (
|
|
865
881
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
866
882
|
{filteredAlunos.map((aluno) => {
|
|
@@ -876,7 +892,7 @@ export default function TurmaDetalhePage() {
|
|
|
876
892
|
<div className="relative">
|
|
877
893
|
<Avatar className="size-12">
|
|
878
894
|
<AvatarImage src={aluno.avatar} />
|
|
879
|
-
<AvatarFallback className="bg-
|
|
895
|
+
<AvatarFallback className="bg-linear-to-br from-blue-100 to-blue-200 text-blue-700 font-medium">
|
|
880
896
|
{aluno.nome
|
|
881
897
|
.split(' ')
|
|
882
898
|
.map((n) => n[0])
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
EmptyState,
|
|
5
|
+
Page,
|
|
6
|
+
PageHeader,
|
|
7
|
+
PaginationFooter,
|
|
8
|
+
} from '@/components/entity-list';
|
|
4
9
|
import { Badge } from '@/components/ui/badge';
|
|
5
10
|
import { Button } from '@/components/ui/button';
|
|
6
11
|
import { Card, CardContent } from '@/components/ui/card';
|
|
@@ -43,10 +48,6 @@ import {
|
|
|
43
48
|
AlertTriangle,
|
|
44
49
|
BarChart3,
|
|
45
50
|
Calendar,
|
|
46
|
-
ChevronLeft,
|
|
47
|
-
ChevronRight,
|
|
48
|
-
ChevronsLeft,
|
|
49
|
-
ChevronsRight,
|
|
50
51
|
Clock,
|
|
51
52
|
Eye,
|
|
52
53
|
Laptop,
|
|
@@ -661,7 +662,7 @@ export default function TurmasPage() {
|
|
|
661
662
|
</div>
|
|
662
663
|
|
|
663
664
|
{/* Search bar */}
|
|
664
|
-
<form onSubmit={handleSearch} className="
|
|
665
|
+
<form onSubmit={handleSearch} className="mb-6 mt-0">
|
|
665
666
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
666
667
|
<div className="relative flex-1">
|
|
667
668
|
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
@@ -711,7 +712,7 @@ export default function TurmasPage() {
|
|
|
711
712
|
value={filtroCursoInput}
|
|
712
713
|
onValueChange={setFiltroCursoInput}
|
|
713
714
|
>
|
|
714
|
-
<SelectTrigger className="h-9 w-
|
|
715
|
+
<SelectTrigger className="h-9 w-40 text-sm">
|
|
715
716
|
<SelectValue placeholder={t('filters.course')} />
|
|
716
717
|
</SelectTrigger>
|
|
717
718
|
<SelectContent>
|
|
@@ -759,17 +760,14 @@ export default function TurmasPage() {
|
|
|
759
760
|
))}
|
|
760
761
|
</div>
|
|
761
762
|
) : filteredTurmas.length === 0 ? (
|
|
762
|
-
<
|
|
763
|
-
<Users className="
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
<
|
|
769
|
-
|
|
770
|
-
{t('empty.action')}
|
|
771
|
-
</Button>
|
|
772
|
-
</div>
|
|
763
|
+
<EmptyState
|
|
764
|
+
icon={<Users className="h-12 w-12" />}
|
|
765
|
+
title={t('empty.title')}
|
|
766
|
+
description={t('empty.description')}
|
|
767
|
+
actionLabel={t('empty.action')}
|
|
768
|
+
onAction={openCreateSheet}
|
|
769
|
+
actionIcon={<Plus className="mr-2 h-4 w-4" />}
|
|
770
|
+
/>
|
|
773
771
|
) : (
|
|
774
772
|
<motion.div
|
|
775
773
|
className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
|
|
@@ -946,87 +944,18 @@ export default function TurmasPage() {
|
|
|
946
944
|
|
|
947
945
|
{/* Pagination footer */}
|
|
948
946
|
{!loading && filteredTurmas.length > 0 && (
|
|
949
|
-
<div className="mt-6
|
|
950
|
-
<
|
|
951
|
-
{
|
|
952
|
-
{
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
{
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
variant="outline"
|
|
962
|
-
size="icon"
|
|
963
|
-
className="size-8"
|
|
964
|
-
onClick={() => setCurrentPage(1)}
|
|
965
|
-
disabled={safePage === 1}
|
|
966
|
-
aria-label={t('pagination.firstPage')}
|
|
967
|
-
>
|
|
968
|
-
<ChevronsLeft className="size-4" />
|
|
969
|
-
</Button>
|
|
970
|
-
<Button
|
|
971
|
-
variant="outline"
|
|
972
|
-
size="icon"
|
|
973
|
-
className="size-8"
|
|
974
|
-
onClick={() => setCurrentPage((p) => p - 1)}
|
|
975
|
-
disabled={safePage === 1}
|
|
976
|
-
aria-label={t('pagination.previousPage')}
|
|
977
|
-
>
|
|
978
|
-
<ChevronLeft className="size-4" />
|
|
979
|
-
</Button>
|
|
980
|
-
<span className="px-3 text-sm">
|
|
981
|
-
{t('pagination.page')}{' '}
|
|
982
|
-
<span className="font-semibold">{safePage}</span>{' '}
|
|
983
|
-
{t('pagination.of')}{' '}
|
|
984
|
-
<span className="font-semibold">{totalPages}</span>
|
|
985
|
-
</span>
|
|
986
|
-
<Button
|
|
987
|
-
variant="outline"
|
|
988
|
-
size="icon"
|
|
989
|
-
className="size-8"
|
|
990
|
-
onClick={() => setCurrentPage((p) => p + 1)}
|
|
991
|
-
disabled={safePage === totalPages}
|
|
992
|
-
aria-label={t('pagination.nextPage')}
|
|
993
|
-
>
|
|
994
|
-
<ChevronRight className="size-4" />
|
|
995
|
-
</Button>
|
|
996
|
-
<Button
|
|
997
|
-
variant="outline"
|
|
998
|
-
size="icon"
|
|
999
|
-
className="size-8"
|
|
1000
|
-
onClick={() => setCurrentPage(totalPages)}
|
|
1001
|
-
disabled={safePage === totalPages}
|
|
1002
|
-
aria-label={t('pagination.lastPage')}
|
|
1003
|
-
>
|
|
1004
|
-
<ChevronsRight className="size-4" />
|
|
1005
|
-
</Button>
|
|
1006
|
-
</div>
|
|
1007
|
-
<div className="flex items-center gap-2 text-sm">
|
|
1008
|
-
<span className="text-muted-foreground">
|
|
1009
|
-
{t('pagination.itemsPerPage')}
|
|
1010
|
-
</span>
|
|
1011
|
-
<Select
|
|
1012
|
-
value={String(pageSize)}
|
|
1013
|
-
onValueChange={(v) => {
|
|
1014
|
-
setPageSize(Number(v));
|
|
1015
|
-
setCurrentPage(1);
|
|
1016
|
-
}}
|
|
1017
|
-
>
|
|
1018
|
-
<SelectTrigger className="h-8 w-16 text-sm">
|
|
1019
|
-
<SelectValue />
|
|
1020
|
-
</SelectTrigger>
|
|
1021
|
-
<SelectContent>
|
|
1022
|
-
{PAGE_SIZES.map((s) => (
|
|
1023
|
-
<SelectItem key={s} value={String(s)}>
|
|
1024
|
-
{s}
|
|
1025
|
-
</SelectItem>
|
|
1026
|
-
))}
|
|
1027
|
-
</SelectContent>
|
|
1028
|
-
</Select>
|
|
1029
|
-
</div>
|
|
947
|
+
<div className="mt-6">
|
|
948
|
+
<PaginationFooter
|
|
949
|
+
currentPage={safePage}
|
|
950
|
+
pageSize={pageSize}
|
|
951
|
+
totalItems={filteredTurmas.length}
|
|
952
|
+
onPageChange={setCurrentPage}
|
|
953
|
+
onPageSizeChange={(nextSize) => {
|
|
954
|
+
setPageSize(nextSize);
|
|
955
|
+
setCurrentPage(1);
|
|
956
|
+
}}
|
|
957
|
+
pageSizeOptions={PAGE_SIZES}
|
|
958
|
+
/>
|
|
1030
959
|
</div>
|
|
1031
960
|
)}
|
|
1032
961
|
|
|
@@ -78,6 +78,120 @@ import {
|
|
|
78
78
|
import { toast } from 'sonner';
|
|
79
79
|
import { z } from 'zod';
|
|
80
80
|
|
|
81
|
+
type MediaUploadFieldProps = {
|
|
82
|
+
label: string;
|
|
83
|
+
description: string;
|
|
84
|
+
uploadLabel: string;
|
|
85
|
+
changeLabel: string;
|
|
86
|
+
removeLabel: string;
|
|
87
|
+
emptyTitle: string;
|
|
88
|
+
preview: string | null;
|
|
89
|
+
previewAlt: string;
|
|
90
|
+
inputRef: React.RefObject<HTMLInputElement | null>;
|
|
91
|
+
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
92
|
+
onClear: () => void;
|
|
93
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
94
|
+
aspectClassName: string;
|
|
95
|
+
compactPreview?: boolean;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function MediaUploadField({
|
|
99
|
+
label,
|
|
100
|
+
description,
|
|
101
|
+
uploadLabel,
|
|
102
|
+
changeLabel,
|
|
103
|
+
removeLabel,
|
|
104
|
+
emptyTitle,
|
|
105
|
+
preview,
|
|
106
|
+
previewAlt,
|
|
107
|
+
inputRef,
|
|
108
|
+
onInputChange,
|
|
109
|
+
onClear,
|
|
110
|
+
icon: Icon,
|
|
111
|
+
aspectClassName,
|
|
112
|
+
compactPreview = false,
|
|
113
|
+
}: MediaUploadFieldProps) {
|
|
114
|
+
return (
|
|
115
|
+
<Field>
|
|
116
|
+
<FieldLabel>{label}</FieldLabel>
|
|
117
|
+
<FieldDescription>{description}</FieldDescription>
|
|
118
|
+
<input
|
|
119
|
+
ref={inputRef}
|
|
120
|
+
type="file"
|
|
121
|
+
accept="image/*"
|
|
122
|
+
className="hidden"
|
|
123
|
+
onChange={onInputChange}
|
|
124
|
+
/>
|
|
125
|
+
<div className="rounded-xl border border-border/70 bg-muted/20 p-4">
|
|
126
|
+
<div
|
|
127
|
+
className={
|
|
128
|
+
compactPreview
|
|
129
|
+
? 'flex flex-col gap-4 sm:flex-row'
|
|
130
|
+
: 'flex flex-col gap-4'
|
|
131
|
+
}
|
|
132
|
+
>
|
|
133
|
+
<div
|
|
134
|
+
className={[
|
|
135
|
+
'overflow-hidden rounded-xl border border-dashed border-border/80 bg-background',
|
|
136
|
+
compactPreview ? 'w-full shrink-0 sm:w-28' : 'w-full',
|
|
137
|
+
].join(' ')}
|
|
138
|
+
>
|
|
139
|
+
{preview ? (
|
|
140
|
+
<img
|
|
141
|
+
src={preview}
|
|
142
|
+
alt={previewAlt}
|
|
143
|
+
className={`${aspectClassName} w-full object-cover`}
|
|
144
|
+
/>
|
|
145
|
+
) : (
|
|
146
|
+
<div
|
|
147
|
+
className={`${aspectClassName} flex w-full flex-col items-center justify-center gap-2 px-4 text-center`}
|
|
148
|
+
>
|
|
149
|
+
<div className="flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
150
|
+
<Icon className="size-5" />
|
|
151
|
+
</div>
|
|
152
|
+
<div className="space-y-1">
|
|
153
|
+
<p className="text-sm font-medium">{emptyTitle}</p>
|
|
154
|
+
<p className="text-xs text-muted-foreground">{uploadLabel}</p>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="flex min-w-0 flex-1 flex-col justify-between gap-3">
|
|
161
|
+
<div className="space-y-1">
|
|
162
|
+
<p className="text-sm font-medium text-foreground">{label}</p>
|
|
163
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div className="flex flex-col gap-2 sm:flex-row">
|
|
167
|
+
<Button
|
|
168
|
+
type="button"
|
|
169
|
+
variant={preview ? 'outline' : 'default'}
|
|
170
|
+
className="w-full gap-2 sm:w-auto"
|
|
171
|
+
onClick={() => inputRef.current?.click()}
|
|
172
|
+
>
|
|
173
|
+
<Upload className="size-4" />
|
|
174
|
+
{preview ? changeLabel : uploadLabel}
|
|
175
|
+
</Button>
|
|
176
|
+
{preview && (
|
|
177
|
+
<Button
|
|
178
|
+
type="button"
|
|
179
|
+
variant="outline"
|
|
180
|
+
className="w-full gap-2 text-destructive hover:text-destructive sm:w-auto"
|
|
181
|
+
onClick={onClear}
|
|
182
|
+
>
|
|
183
|
+
<XCircle className="size-4" />
|
|
184
|
+
{removeLabel}
|
|
185
|
+
</Button>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</Field>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
81
195
|
// ── Navigation ──────────────────────────────────────────────────────────────────
|
|
82
196
|
|
|
83
197
|
const NAV_ITEMS = [
|
|
@@ -773,7 +887,7 @@ export default function CursoEditPage({
|
|
|
773
887
|
return (
|
|
774
888
|
<label
|
|
775
889
|
key={cat}
|
|
776
|
-
className="flex cursor-pointer items-center gap-2.5 rounded-md border px-3 py-2.5 text-sm transition-colors hover:bg-muted/50 has-
|
|
890
|
+
className="flex cursor-pointer items-center gap-2.5 rounded-md border px-3 py-2.5 text-sm transition-colors hover:bg-muted/50 has-checked:border-foreground has-checked:bg-muted/50"
|
|
777
891
|
>
|
|
778
892
|
<Checkbox
|
|
779
893
|
checked={checked}
|
|
@@ -824,7 +938,7 @@ export default function CursoEditPage({
|
|
|
824
938
|
return (
|
|
825
939
|
<label
|
|
826
940
|
key={inst.id}
|
|
827
|
-
className="flex cursor-pointer items-center gap-2.5 rounded-md border px-3 py-2.5 text-sm transition-colors hover:bg-muted/50 has-
|
|
941
|
+
className="flex cursor-pointer items-center gap-2.5 rounded-md border px-3 py-2.5 text-sm transition-colors hover:bg-muted/50 has-checked:border-foreground has-checked:bg-muted/50"
|
|
828
942
|
>
|
|
829
943
|
<Checkbox
|
|
830
944
|
checked={checked}
|
|
@@ -882,114 +996,43 @@ export default function CursoEditPage({
|
|
|
882
996
|
<Separator />
|
|
883
997
|
|
|
884
998
|
{/* ── Upload Logo + Banner ─────────────────────────────── */}
|
|
885
|
-
<div className="grid gap-4
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
{
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
className="aspect-square w-full object-cover"
|
|
905
|
-
/>
|
|
906
|
-
<div className="absolute inset-0 flex items-center justify-center gap-2 bg-background/80 opacity-0 transition-opacity group-hover:opacity-100">
|
|
907
|
-
<Button
|
|
908
|
-
type="button"
|
|
909
|
-
variant="outline"
|
|
910
|
-
size="sm"
|
|
911
|
-
onClick={() => logoInputRef.current?.click()}
|
|
912
|
-
>
|
|
913
|
-
{t('form.fields.logo.change')}
|
|
914
|
-
</Button>
|
|
915
|
-
<Button
|
|
916
|
-
type="button"
|
|
917
|
-
variant="outline"
|
|
918
|
-
size="sm"
|
|
919
|
-
onClick={() => setLogoPreview(null)}
|
|
920
|
-
>
|
|
921
|
-
<XCircle className="size-4" />
|
|
922
|
-
</Button>
|
|
923
|
-
</div>
|
|
924
|
-
</div>
|
|
925
|
-
) : (
|
|
926
|
-
<button
|
|
927
|
-
type="button"
|
|
928
|
-
onClick={() => logoInputRef.current?.click()}
|
|
929
|
-
className="flex aspect-square w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed transition-colors hover:border-foreground/30 hover:bg-muted/50"
|
|
930
|
-
>
|
|
931
|
-
<Upload className="size-8 text-muted-foreground/50" />
|
|
932
|
-
<span className="text-xs text-muted-foreground">
|
|
933
|
-
{t('form.fields.logo.clickToUpload')}
|
|
934
|
-
</span>
|
|
935
|
-
</button>
|
|
936
|
-
)}
|
|
937
|
-
</Field>
|
|
999
|
+
<div className="grid gap-4 xl:grid-cols-[minmax(0,0.95fr)_minmax(0,1.25fr)]">
|
|
1000
|
+
<MediaUploadField
|
|
1001
|
+
label={t('form.fields.logo.label')}
|
|
1002
|
+
description={t('form.fields.logo.description')}
|
|
1003
|
+
uploadLabel={t('form.fields.logo.clickToUpload')}
|
|
1004
|
+
changeLabel={t('form.fields.logo.change')}
|
|
1005
|
+
removeLabel={t('form.fields.logo.remove')}
|
|
1006
|
+
emptyTitle={t('form.fields.logo.emptyTitle')}
|
|
1007
|
+
preview={logoPreview}
|
|
1008
|
+
previewAlt={t('form.fields.logo.previewAlt')}
|
|
1009
|
+
inputRef={logoInputRef}
|
|
1010
|
+
onInputChange={(e) =>
|
|
1011
|
+
handleFileSelect(e, setLogoPreview)
|
|
1012
|
+
}
|
|
1013
|
+
onClear={() => setLogoPreview(null)}
|
|
1014
|
+
icon={ImageIcon}
|
|
1015
|
+
aspectClassName="aspect-square"
|
|
1016
|
+
compactPreview
|
|
1017
|
+
/>
|
|
938
1018
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
<img
|
|
957
|
-
src={bannerPreview}
|
|
958
|
-
alt="Banner preview"
|
|
959
|
-
className="aspect-video w-full object-cover"
|
|
960
|
-
/>
|
|
961
|
-
<div className="absolute inset-0 flex items-center justify-center gap-2 bg-background/80 opacity-0 transition-opacity group-hover:opacity-100">
|
|
962
|
-
<Button
|
|
963
|
-
type="button"
|
|
964
|
-
variant="outline"
|
|
965
|
-
size="sm"
|
|
966
|
-
onClick={() => bannerInputRef.current?.click()}
|
|
967
|
-
>
|
|
968
|
-
{t('form.fields.banner.change')}
|
|
969
|
-
</Button>
|
|
970
|
-
<Button
|
|
971
|
-
type="button"
|
|
972
|
-
variant="outline"
|
|
973
|
-
size="sm"
|
|
974
|
-
onClick={() => setBannerPreview(null)}
|
|
975
|
-
>
|
|
976
|
-
<XCircle className="size-4" />
|
|
977
|
-
</Button>
|
|
978
|
-
</div>
|
|
979
|
-
</div>
|
|
980
|
-
) : (
|
|
981
|
-
<button
|
|
982
|
-
type="button"
|
|
983
|
-
onClick={() => bannerInputRef.current?.click()}
|
|
984
|
-
className="flex aspect-video w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed transition-colors hover:border-foreground/30 hover:bg-muted/50"
|
|
985
|
-
>
|
|
986
|
-
<ImageIcon className="size-8 text-muted-foreground/50" />
|
|
987
|
-
<span className="text-xs text-muted-foreground">
|
|
988
|
-
{t('form.fields.banner.clickToUpload')}
|
|
989
|
-
</span>
|
|
990
|
-
</button>
|
|
991
|
-
)}
|
|
992
|
-
</Field>
|
|
1019
|
+
<MediaUploadField
|
|
1020
|
+
label={t('form.fields.banner.label')}
|
|
1021
|
+
description={t('form.fields.banner.description')}
|
|
1022
|
+
uploadLabel={t('form.fields.banner.clickToUpload')}
|
|
1023
|
+
changeLabel={t('form.fields.banner.change')}
|
|
1024
|
+
removeLabel={t('form.fields.banner.remove')}
|
|
1025
|
+
emptyTitle={t('form.fields.banner.emptyTitle')}
|
|
1026
|
+
preview={bannerPreview}
|
|
1027
|
+
previewAlt={t('form.fields.banner.previewAlt')}
|
|
1028
|
+
inputRef={bannerInputRef}
|
|
1029
|
+
onInputChange={(e) =>
|
|
1030
|
+
handleFileSelect(e, setBannerPreview)
|
|
1031
|
+
}
|
|
1032
|
+
onClear={() => setBannerPreview(null)}
|
|
1033
|
+
icon={ImageIcon}
|
|
1034
|
+
aspectClassName="aspect-[16/9]"
|
|
1035
|
+
/>
|
|
993
1036
|
</div>
|
|
994
1037
|
|
|
995
1038
|
<Separator />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Page, PageHeader } from '@/components/entity-list';
|
|
3
|
+
import { EmptyState, Page, PageHeader } from '@/components/entity-list';
|
|
4
4
|
import { Badge } from '@/components/ui/badge';
|
|
5
5
|
import { Button } from '@/components/ui/button';
|
|
6
6
|
import { Card, CardContent } from '@/components/ui/card';
|
|
@@ -1310,23 +1310,16 @@ export default function EstruturaPage({
|
|
|
1310
1310
|
|
|
1311
1311
|
{/* Empty state */}
|
|
1312
1312
|
{sessoes.length === 0 && (
|
|
1313
|
-
<motion.div
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
{t('empty.description')}
|
|
1324
|
-
</p>
|
|
1325
|
-
</div>
|
|
1326
|
-
<Button className="gap-2" onClick={openCreateSessao}>
|
|
1327
|
-
<Plus className="size-4" />
|
|
1328
|
-
{t('empty.action')}
|
|
1329
|
-
</Button>
|
|
1313
|
+
<motion.div variants={fadeUp} className="mt-8">
|
|
1314
|
+
<EmptyState
|
|
1315
|
+
icon={<Layers className="h-12 w-12" />}
|
|
1316
|
+
title={t('empty.title')}
|
|
1317
|
+
description={t('empty.description')}
|
|
1318
|
+
actionLabel={t('empty.action')}
|
|
1319
|
+
actionIcon={<Plus className="size-4" />}
|
|
1320
|
+
onAction={openCreateSessao}
|
|
1321
|
+
className="py-16"
|
|
1322
|
+
/>
|
|
1330
1323
|
</motion.div>
|
|
1331
1324
|
)}
|
|
1332
1325
|
</motion.div>
|
|
@@ -2429,20 +2422,15 @@ function SortableSessao({
|
|
|
2429
2422
|
>
|
|
2430
2423
|
<div className="flex flex-col gap-0 p-2">
|
|
2431
2424
|
{aulas.length === 0 ? (
|
|
2432
|
-
<
|
|
2433
|
-
<
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
>
|
|
2442
|
-
<Plus className="size-3" />
|
|
2443
|
-
{t('session.addLesson')}
|
|
2444
|
-
</Button>
|
|
2445
|
-
</div>
|
|
2425
|
+
<EmptyState
|
|
2426
|
+
icon={<Layers className="h-10 w-10" />}
|
|
2427
|
+
title={t('session.noLessons')}
|
|
2428
|
+
description={t('session.noLessonsDescription')}
|
|
2429
|
+
actionLabel={t('session.addLesson')}
|
|
2430
|
+
actionIcon={<Plus className="size-3.5" />}
|
|
2431
|
+
onAction={onCreateAula}
|
|
2432
|
+
className="py-8"
|
|
2433
|
+
/>
|
|
2446
2434
|
) : (
|
|
2447
2435
|
aulas.map((aula) => (
|
|
2448
2436
|
<SortableAula
|