@hed-hog/lms 0.0.279 → 0.0.286
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 +42 -21
- 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 +22 -34
- package/hedhog/frontend/app/courses/page.tsx.ejs +31 -106
- package/hedhog/frontend/app/exams/[id]/attempt/page.tsx.ejs +25 -3
- 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/page.tsx.ejs +3 -3
- 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 +6 -6
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';
|
|
@@ -215,9 +215,10 @@ function generateAulas(): Aula[] {
|
|
|
215
215
|
for (let i = -10; i < 20; i++) {
|
|
216
216
|
const dia = addDays(today, i);
|
|
217
217
|
if (dia.getDay() === 0 || dia.getDay() === 6) continue;
|
|
218
|
+
const titulo = titulos[aulas.length % titulos.length]!;
|
|
218
219
|
aulas.push({
|
|
219
220
|
id: aulas.length + 1,
|
|
220
|
-
titulo
|
|
221
|
+
titulo,
|
|
221
222
|
data: dia,
|
|
222
223
|
horaInicio: '19:00',
|
|
223
224
|
horaFim: '22:00',
|
|
@@ -330,13 +331,15 @@ export default function TurmaDetalhePage() {
|
|
|
330
331
|
// Calendar events
|
|
331
332
|
const calendarEvents = useMemo(() => {
|
|
332
333
|
return aulas.map((aula) => {
|
|
333
|
-
const [
|
|
334
|
-
|
|
334
|
+
const [startHour = 0, startMinute = 0] = aula.horaInicio
|
|
335
|
+
.split(':')
|
|
336
|
+
.map(Number);
|
|
337
|
+
const [endHour = 0, endMinute = 0] = aula.horaFim.split(':').map(Number);
|
|
335
338
|
return {
|
|
336
339
|
id: aula.id,
|
|
337
340
|
title: aula.titulo,
|
|
338
|
-
start: setMinutes(setHours(aula.data,
|
|
339
|
-
end: setMinutes(setHours(aula.data,
|
|
341
|
+
start: setMinutes(setHours(aula.data, startHour), startMinute),
|
|
342
|
+
end: setMinutes(setHours(aula.data, endHour), endMinute),
|
|
340
343
|
resource: aula,
|
|
341
344
|
};
|
|
342
345
|
});
|
|
@@ -546,7 +549,7 @@ export default function TurmaDetalhePage() {
|
|
|
546
549
|
},
|
|
547
550
|
];
|
|
548
551
|
|
|
549
|
-
const STATUS_MAP
|
|
552
|
+
const STATUS_MAP = {
|
|
550
553
|
aberta: {
|
|
551
554
|
label: tClasses('status.aberta'),
|
|
552
555
|
color: 'bg-blue-100 text-blue-700 border-blue-200',
|
|
@@ -563,7 +566,9 @@ export default function TurmaDetalhePage() {
|
|
|
563
566
|
label: tClasses('status.cancelada'),
|
|
564
567
|
color: 'bg-red-100 text-red-700 border-red-200',
|
|
565
568
|
},
|
|
566
|
-
};
|
|
569
|
+
} as const;
|
|
570
|
+
const turmaStatus =
|
|
571
|
+
STATUS_MAP[turma.status as keyof typeof STATUS_MAP] ?? STATUS_MAP.aberta;
|
|
567
572
|
|
|
568
573
|
const fadeUp = {
|
|
569
574
|
hidden: { opacity: 0, y: 20 },
|
|
@@ -616,8 +621,8 @@ export default function TurmaDetalhePage() {
|
|
|
616
621
|
>
|
|
617
622
|
<div>
|
|
618
623
|
<div className="flex flex-wrap items-center gap-2 mb-1">
|
|
619
|
-
<Badge className={`${
|
|
620
|
-
{
|
|
624
|
+
<Badge className={`${turmaStatus.color} border`}>
|
|
625
|
+
{turmaStatus.label}
|
|
621
626
|
</Badge>
|
|
622
627
|
</div>
|
|
623
628
|
<p className="text-muted-foreground">
|
|
@@ -851,16 +856,32 @@ export default function TurmaDetalhePage() {
|
|
|
851
856
|
))}
|
|
852
857
|
</div>
|
|
853
858
|
) : filteredAlunos.length === 0 ? (
|
|
854
|
-
<
|
|
855
|
-
<
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
859
|
+
<EmptyState
|
|
860
|
+
icon={<Users className="h-12 w-12" />}
|
|
861
|
+
title={
|
|
862
|
+
alunoSearch
|
|
863
|
+
? t('students.empty.notFound')
|
|
864
|
+
: t('students.empty.notEnrolled')
|
|
865
|
+
}
|
|
866
|
+
description={
|
|
867
|
+
alunoSearch
|
|
868
|
+
? t('students.empty.notFoundDescription')
|
|
869
|
+
: t('students.empty.notEnrolledDescription')
|
|
870
|
+
}
|
|
871
|
+
actionLabel={
|
|
872
|
+
alunoSearch
|
|
873
|
+
? t('students.empty.clearSearch')
|
|
874
|
+
: t('students.actions.addStudent')
|
|
875
|
+
}
|
|
876
|
+
onAction={() => {
|
|
877
|
+
if (alunoSearch) {
|
|
878
|
+
setAlunoSearch('');
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
setAddAlunoDialogOpen(true);
|
|
883
|
+
}}
|
|
884
|
+
/>
|
|
864
885
|
) : (
|
|
865
886
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
866
887
|
{filteredAlunos.map((aluno) => {
|
|
@@ -876,7 +897,7 @@ export default function TurmaDetalhePage() {
|
|
|
876
897
|
<div className="relative">
|
|
877
898
|
<Avatar className="size-12">
|
|
878
899
|
<AvatarImage src={aluno.avatar} />
|
|
879
|
-
<AvatarFallback className="bg-
|
|
900
|
+
<AvatarFallback className="bg-linear-to-br from-blue-100 to-blue-200 text-blue-700 font-medium">
|
|
880
901
|
{aluno.nome
|
|
881
902
|
.split(' ')
|
|
882
903
|
.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 />
|