@hed-hog/core 0.0.185 → 0.0.190

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 (55) hide show
  1. package/hedhog/frontend/app/account/2fa/page.tsx.ejs +5 -0
  2. package/hedhog/frontend/app/account/accounts/page.tsx.ejs +5 -0
  3. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +356 -0
  4. package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +379 -0
  5. package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +184 -0
  6. package/hedhog/frontend/app/account/components/connected-accounts.tsx.ejs +144 -0
  7. package/hedhog/frontend/app/account/components/email-request-dialog.tsx.ejs +96 -0
  8. package/hedhog/frontend/app/account/components/mfa-add-buttons.tsx.ejs +43 -0
  9. package/hedhog/frontend/app/account/components/mfa-method-card.tsx.ejs +115 -0
  10. package/hedhog/frontend/app/account/components/mfa-setup-dialog.tsx.ejs +236 -0
  11. package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +209 -0
  12. package/hedhog/frontend/app/account/components/recovery-codes-dialog.tsx.ejs +192 -0
  13. package/hedhog/frontend/app/account/components/regenerate-codes-dialog.tsx.ejs +372 -0
  14. package/hedhog/frontend/app/account/components/remove-mfa-dialog.tsx.ejs +337 -0
  15. package/hedhog/frontend/app/account/components/two-factor-auth.tsx.ejs +393 -0
  16. package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +332 -0
  17. package/hedhog/frontend/app/account/email/page.tsx.ejs +5 -0
  18. package/hedhog/frontend/app/account/hooks/use-mfa-methods.ts.ejs +27 -0
  19. package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +461 -0
  20. package/hedhog/frontend/app/account/layout.tsx.ejs +105 -0
  21. package/hedhog/frontend/app/account/lib/mfa-utils.tsx.ejs +37 -0
  22. package/hedhog/frontend/app/account/page.tsx.ejs +5 -0
  23. package/hedhog/frontend/app/account/password/page.tsx.ejs +5 -0
  24. package/hedhog/frontend/app/account/profile/page.tsx.ejs +5 -0
  25. package/hedhog/frontend/app/account/sessions/page.tsx.ejs +5 -0
  26. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +490 -0
  27. package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +62 -0
  28. package/hedhog/frontend/app/configurations/layout.tsx.ejs +316 -0
  29. package/hedhog/frontend/app/configurations/page.tsx.ejs +35 -0
  30. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +351 -0
  31. package/hedhog/frontend/app/dashboard/[slug]/page.tsx.ejs +11 -0
  32. package/hedhog/frontend/app/dashboard/[slug]/types.ts.ejs +62 -0
  33. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +45 -0
  34. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +196 -0
  35. package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +63 -0
  36. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +516 -0
  37. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +753 -0
  38. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +516 -0
  39. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +489 -0
  40. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +621 -0
  41. package/hedhog/frontend/app/dashboard/page.tsx.ejs +14 -0
  42. package/hedhog/frontend/app/mail/log/page.tsx.ejs +312 -0
  43. package/hedhog/frontend/app/mail/template/page.tsx.ejs +1177 -0
  44. package/hedhog/frontend/app/preferences/page.tsx.ejs +448 -0
  45. package/hedhog/frontend/app/roles/menus.tsx.ejs +504 -0
  46. package/hedhog/frontend/app/roles/page.tsx.ejs +814 -0
  47. package/hedhog/frontend/app/roles/routes.tsx.ejs +397 -0
  48. package/hedhog/frontend/app/roles/users.tsx.ejs +306 -0
  49. package/hedhog/frontend/app/users/active-session.tsx.ejs +159 -0
  50. package/hedhog/frontend/app/users/identifiers.tsx.ejs +279 -0
  51. package/hedhog/frontend/app/users/page.tsx.ejs +1257 -0
  52. package/hedhog/frontend/app/users/permissions.tsx.ejs +155 -0
  53. package/hedhog/frontend/messages/en.json +1080 -0
  54. package/hedhog/frontend/messages/pt.json +1135 -0
  55. package/package.json +4 -4
@@ -0,0 +1,753 @@
1
+ 'use client';
2
+
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogAction,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ } from '@/components/ui/alert-dialog';
13
+ import { Button } from '@/components/ui/button';
14
+ import { Checkbox } from '@/components/ui/checkbox';
15
+ import {
16
+ Dialog,
17
+ DialogContent,
18
+ DialogDescription,
19
+ DialogFooter,
20
+ DialogHeader,
21
+ DialogTitle,
22
+ DialogTrigger,
23
+ } from '@/components/ui/dialog';
24
+ import { Input } from '@/components/ui/input';
25
+ import { Label } from '@/components/ui/label';
26
+ import {
27
+ Select,
28
+ SelectContent,
29
+ SelectItem,
30
+ SelectTrigger,
31
+ SelectValue,
32
+ } from '@/components/ui/select';
33
+ import {
34
+ Table,
35
+ TableBody,
36
+ TableCell,
37
+ TableHead,
38
+ TableHeader,
39
+ TableRow,
40
+ } from '@/components/ui/table';
41
+ import { useDebounce } from '@/hooks/use-debounce';
42
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
43
+ import * as TablerIcons from '@tabler/icons-react';
44
+ import {
45
+ IconChevronLeft,
46
+ IconChevronRight,
47
+ IconChevronsLeft,
48
+ IconChevronsRight,
49
+ IconEdit,
50
+ IconGlobe,
51
+ IconPlus,
52
+ IconTrash,
53
+ } from '@tabler/icons-react';
54
+ import { useTranslations } from 'next-intl';
55
+ import { useState } from 'react';
56
+ import { toast } from 'sonner';
57
+
58
+ interface Component {
59
+ id: number;
60
+ slug: string;
61
+ path: string;
62
+ min_width: number | null;
63
+ max_width: number | null;
64
+ min_height: number | null;
65
+ max_height: number | null;
66
+ width: number;
67
+ height: number;
68
+ is_resizable: boolean;
69
+ stat_key?: string;
70
+ icon?: string;
71
+ color?: string;
72
+ dashboard_component_locale: Array<{
73
+ locale: { code: string };
74
+ name: string;
75
+ description?: string;
76
+ }>;
77
+ }
78
+
79
+ export function ComponentsTab() {
80
+ const t = useTranslations('core.DashboardManagement');
81
+ const { request, locales, currentLocaleCode } = useApp();
82
+ const [open, setOpen] = useState(false);
83
+ const [selectedComponent, setSelectedComponent] = useState<any | null>(null);
84
+ const [isNew, setIsNew] = useState(false);
85
+ const [selectedLocale, setSelectedLocale] = useState(currentLocaleCode);
86
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
87
+ const [componentToDelete, setComponentToDelete] = useState<number | null>(
88
+ null
89
+ );
90
+ const [page, setPage] = useState(1);
91
+ const [pageSize, setPageSize] = useState(10);
92
+ const [searchQuery, setSearchQuery] = useState('');
93
+ const debouncedSearch = useDebounce(searchQuery, 500);
94
+
95
+ const renderIcon = (iconName?: string) => {
96
+ if (!iconName) {
97
+ return <TablerIcons.IconBox className="h-4 w-4" />;
98
+ }
99
+
100
+ const toPascalCase = (str: string) =>
101
+ str.replace(/(^\w|-\w)/g, (match) =>
102
+ match.replace('-', '').toUpperCase()
103
+ );
104
+
105
+ const pascalName = toPascalCase(String(iconName));
106
+
107
+ let IconComponent = (TablerIcons as any)[`Icon${pascalName}`];
108
+ if (!IconComponent) {
109
+ IconComponent = (TablerIcons as any)[`Icon${pascalName}Filled`];
110
+ }
111
+ if (!IconComponent) {
112
+ IconComponent = (TablerIcons as any)[`Icon${pascalName}Circle`];
113
+ }
114
+
115
+ if (IconComponent) {
116
+ return <IconComponent className="h-4 w-4" />;
117
+ }
118
+
119
+ return <TablerIcons.IconBox className="h-4 w-4" />;
120
+ };
121
+
122
+ const {
123
+ data: paginatedData,
124
+ isLoading,
125
+ refetch,
126
+ } = useQuery<{ data: Component[]; total: number }>({
127
+ queryKey: [
128
+ 'dashboard-components',
129
+ page,
130
+ pageSize,
131
+ debouncedSearch,
132
+ currentLocaleCode,
133
+ ],
134
+ queryFn: async () => {
135
+ const params = new URLSearchParams();
136
+ params.set('page', String(page));
137
+ params.set('pageSize', String(pageSize));
138
+ if (debouncedSearch) params.set('search', debouncedSearch);
139
+
140
+ const response = await request<{ data: Component[]; total: number }>({
141
+ url: `/dashboard-component?${params.toString()}`,
142
+ method: 'GET',
143
+ });
144
+ return response.data;
145
+ },
146
+ });
147
+
148
+ const components = paginatedData?.data ?? [];
149
+ const total = paginatedData?.total ?? 0;
150
+ const totalPages = Math.ceil(total / pageSize);
151
+
152
+ const handleNew = () => {
153
+ const newComponent: any = {
154
+ slug: '',
155
+ path: '',
156
+ min_width: 2,
157
+ max_width: 6,
158
+ min_height: 1,
159
+ max_height: 2,
160
+ width: 3,
161
+ height: 2,
162
+ is_resizable: true,
163
+ stat_key: '',
164
+ icon: '',
165
+ color: '#3b82f6',
166
+ locale: {},
167
+ };
168
+
169
+ locales.forEach((locale: any) => {
170
+ newComponent.locale[locale.code] = { name: '', description: '' };
171
+ });
172
+
173
+ setSelectedComponent(newComponent);
174
+ setIsNew(true);
175
+ setSelectedLocale(currentLocaleCode);
176
+ setOpen(true);
177
+ };
178
+
179
+ const handleEdit = async (component: Component) => {
180
+ try {
181
+ const { data } = await request<any>({
182
+ url: `/dashboard-component/${component.id}`,
183
+ method: 'GET',
184
+ });
185
+
186
+ const localeData: Record<string, { name: string; description?: string }> =
187
+ {};
188
+ if (
189
+ data.dashboard_component_locale &&
190
+ Array.isArray(data.dashboard_component_locale)
191
+ ) {
192
+ data.dashboard_component_locale.forEach((dl: any) => {
193
+ const localeCode = locales.find(
194
+ (l: any) => l.code === dl.locale?.code
195
+ )?.code;
196
+ if (localeCode) {
197
+ localeData[localeCode] = {
198
+ name: dl.name || '',
199
+ description: dl.description || '',
200
+ };
201
+ }
202
+ });
203
+ }
204
+
205
+ locales.forEach((locale: any) => {
206
+ if (!localeData[locale.code]) {
207
+ localeData[locale.code] = { name: '', description: '' };
208
+ }
209
+ });
210
+
211
+ setSelectedComponent({
212
+ ...data,
213
+ locale: localeData,
214
+ });
215
+ setIsNew(false);
216
+ setSelectedLocale(currentLocaleCode);
217
+ setOpen(true);
218
+ } catch (error) {
219
+ console.error('Erro ao carregar componente:', error);
220
+ toast.error('Erro ao carregar componente');
221
+ }
222
+ };
223
+
224
+ const handleSave = async () => {
225
+ if (!selectedComponent || !selectedComponent.locale) return;
226
+
227
+ const payload = {
228
+ slug: selectedComponent.slug,
229
+ path: selectedComponent.path,
230
+ min_width: selectedComponent.min_width || null,
231
+ max_width: selectedComponent.max_width || null,
232
+ min_height: selectedComponent.min_height || null,
233
+ max_height: selectedComponent.max_height || null,
234
+ width: selectedComponent.width,
235
+ height: selectedComponent.height,
236
+ is_resizable: selectedComponent.is_resizable ?? true,
237
+ stat_key: selectedComponent.stat_key || null,
238
+ icon: selectedComponent.icon || null,
239
+ color: selectedComponent.color || null,
240
+ locale: selectedComponent.locale,
241
+ };
242
+
243
+ try {
244
+ if (selectedComponent.id) {
245
+ await request({
246
+ url: `/dashboard-component/${selectedComponent.id}`,
247
+ method: 'PATCH',
248
+ data: payload,
249
+ });
250
+ toast.success(t('componentUpdated'));
251
+ } else {
252
+ await request({
253
+ url: '/dashboard-component',
254
+ method: 'POST',
255
+ data: payload,
256
+ });
257
+ toast.success(t('componentCreated'));
258
+ }
259
+
260
+ setOpen(false);
261
+ setSelectedComponent(null);
262
+ refetch();
263
+ } catch (error) {
264
+ console.error('Erro ao salvar componente:', error);
265
+ toast.error('Erro ao salvar componente');
266
+ }
267
+ };
268
+
269
+ const handleDeleteClick = (id: number) => {
270
+ setComponentToDelete(id);
271
+ setDeleteDialogOpen(true);
272
+ };
273
+
274
+ const handleDeleteConfirm = async () => {
275
+ if (!componentToDelete) return;
276
+
277
+ try {
278
+ await request({
279
+ url: `/dashboard-component/${componentToDelete}`,
280
+ method: 'DELETE',
281
+ });
282
+ toast.success(t('componentDeleted'));
283
+ setPage(1);
284
+ refetch();
285
+ } catch (error) {
286
+ console.error('Erro ao excluir componente:', error);
287
+ toast.error('Erro ao excluir componente');
288
+ } finally {
289
+ setDeleteDialogOpen(false);
290
+ setComponentToDelete(null);
291
+ }
292
+ };
293
+
294
+ return (
295
+ <div className="space-y-4">
296
+ <div className="flex justify-between items-center">
297
+ <div>
298
+ <h2 className="text-lg font-semibold">{t('componentsTab')}</h2>
299
+ <p className="text-muted-foreground text-sm">
300
+ {t('manageComponents')}
301
+ </p>
302
+ </div>
303
+ <Dialog open={open} onOpenChange={setOpen}>
304
+ <DialogTrigger asChild>
305
+ <Button size="sm" className="gap-2" onClick={handleNew}>
306
+ <IconPlus className="size-4" />
307
+ {t('newComponent')}
308
+ </Button>
309
+ </DialogTrigger>
310
+ <DialogContent className="max-w-4xl max-h-[95vh] overflow-y-auto">
311
+ <DialogHeader>
312
+ <DialogTitle>
313
+ {isNew ? t('newComponent') : t('editComponent')}
314
+ </DialogTitle>
315
+ <DialogDescription>{t('fillComponentData')}</DialogDescription>
316
+ </DialogHeader>
317
+ {selectedComponent && (
318
+ <div className="space-y-4">
319
+ {!isNew && (
320
+ <div className="flex items-center gap-2">
321
+ <IconGlobe className="size-4" />
322
+ <Select
323
+ value={selectedLocale}
324
+ onValueChange={setSelectedLocale}
325
+ >
326
+ <SelectTrigger className="w-[200px]">
327
+ <SelectValue />
328
+ </SelectTrigger>
329
+ <SelectContent>
330
+ {locales.map((locale: any) => (
331
+ <SelectItem key={locale.code} value={locale.code}>
332
+ {locale.name}
333
+ </SelectItem>
334
+ ))}
335
+ </SelectContent>
336
+ </Select>
337
+ </div>
338
+ )}
339
+ <div className="space-y-2">
340
+ <Label htmlFor="slug">{t('slug')}</Label>
341
+ <Input
342
+ id="slug"
343
+ placeholder="total-users"
344
+ value={selectedComponent.slug || ''}
345
+ onChange={(e) =>
346
+ setSelectedComponent({
347
+ ...selectedComponent,
348
+ slug: e.target.value,
349
+ })
350
+ }
351
+ />
352
+ </div>
353
+
354
+ <div className="space-y-2">
355
+ <Label htmlFor="name">{t('name')}</Label>
356
+ <Input
357
+ id="name"
358
+ placeholder="Total de Usuários"
359
+ value={
360
+ selectedComponent.locale?.[selectedLocale]?.name || ''
361
+ }
362
+ onChange={(e) =>
363
+ setSelectedComponent({
364
+ ...selectedComponent,
365
+ locale: {
366
+ ...selectedComponent.locale,
367
+ [selectedLocale]: {
368
+ ...selectedComponent.locale?.[selectedLocale],
369
+ name: e.target.value,
370
+ },
371
+ },
372
+ })
373
+ }
374
+ />
375
+ </div>
376
+
377
+ <div className="space-y-2">
378
+ <Label htmlFor="description">{t('descriptionLabel')}</Label>
379
+ <Input
380
+ id="description"
381
+ placeholder={t('descriptionPlaceholder')}
382
+ value={
383
+ selectedComponent.locale?.[selectedLocale]?.description ||
384
+ ''
385
+ }
386
+ onChange={(e) =>
387
+ setSelectedComponent({
388
+ ...selectedComponent,
389
+ locale: {
390
+ ...selectedComponent.locale,
391
+ [selectedLocale]: {
392
+ ...selectedComponent.locale?.[selectedLocale],
393
+ description: e.target.value,
394
+ },
395
+ },
396
+ })
397
+ }
398
+ />
399
+ </div>
400
+
401
+ <div className="rounded-md border p-4 bg-muted/50">
402
+ <h4 className="text-sm font-medium mb-3">
403
+ {t('dimensions')}
404
+ </h4>
405
+ <div className="grid grid-cols-2 gap-4 mb-4">
406
+ <div className="space-y-2">
407
+ <Label htmlFor="width">{t('defaultWidth')}</Label>
408
+ <Input
409
+ id="width"
410
+ type="number"
411
+ min="1"
412
+ max="12"
413
+ value={selectedComponent.width || 3}
414
+ onChange={(e) =>
415
+ setSelectedComponent({
416
+ ...selectedComponent,
417
+ width: parseInt(e.target.value) || 3,
418
+ })
419
+ }
420
+ />
421
+ </div>
422
+ <div className="space-y-2">
423
+ <Label htmlFor="height">{t('defaultHeight')}</Label>
424
+ <Input
425
+ id="height"
426
+ type="number"
427
+ min="1"
428
+ value={selectedComponent.height || 2}
429
+ onChange={(e) =>
430
+ setSelectedComponent({
431
+ ...selectedComponent,
432
+ height: parseInt(e.target.value) || 2,
433
+ })
434
+ }
435
+ />
436
+ </div>
437
+ </div>
438
+
439
+ <div className="grid grid-cols-4 gap-4 mb-4">
440
+ <div className="space-y-2">
441
+ <Label htmlFor="min_width">{t('minWidth')}</Label>
442
+ <Input
443
+ id="min_width"
444
+ type="number"
445
+ min="1"
446
+ max="12"
447
+ value={selectedComponent.min_width || ''}
448
+ onChange={(e) =>
449
+ setSelectedComponent({
450
+ ...selectedComponent,
451
+ min_width: e.target.value
452
+ ? parseInt(e.target.value)
453
+ : null,
454
+ })
455
+ }
456
+ />
457
+ </div>
458
+ <div className="space-y-2">
459
+ <Label htmlFor="max_width">{t('maxWidth')}</Label>
460
+ <Input
461
+ id="max_width"
462
+ type="number"
463
+ min="1"
464
+ max="12"
465
+ value={selectedComponent.max_width || ''}
466
+ onChange={(e) =>
467
+ setSelectedComponent({
468
+ ...selectedComponent,
469
+ max_width: e.target.value
470
+ ? parseInt(e.target.value)
471
+ : null,
472
+ })
473
+ }
474
+ />
475
+ </div>
476
+ <div className="space-y-2">
477
+ <Label htmlFor="min_height">{t('minHeight')}</Label>
478
+ <Input
479
+ id="min_height"
480
+ type="number"
481
+ min="1"
482
+ value={selectedComponent.min_height || ''}
483
+ onChange={(e) =>
484
+ setSelectedComponent({
485
+ ...selectedComponent,
486
+ min_height: e.target.value
487
+ ? parseInt(e.target.value)
488
+ : null,
489
+ })
490
+ }
491
+ />
492
+ </div>
493
+ <div className="space-y-2">
494
+ <Label htmlFor="max_height">{t('maxHeight')}</Label>
495
+ <Input
496
+ id="max_height"
497
+ type="number"
498
+ min="1"
499
+ value={selectedComponent.max_height || ''}
500
+ onChange={(e) =>
501
+ setSelectedComponent({
502
+ ...selectedComponent,
503
+ max_height: e.target.value
504
+ ? parseInt(e.target.value)
505
+ : null,
506
+ })
507
+ }
508
+ />
509
+ </div>
510
+ </div>
511
+
512
+ <div className="flex items-center gap-2 mb-3">
513
+ <Checkbox
514
+ id="is_resizable"
515
+ checked={selectedComponent.is_resizable ?? true}
516
+ onCheckedChange={(checked) =>
517
+ setSelectedComponent({
518
+ ...selectedComponent,
519
+ is_resizable: checked === true,
520
+ })
521
+ }
522
+ />
523
+ <Label
524
+ htmlFor="is_resizable"
525
+ className="text-sm font-normal cursor-pointer"
526
+ >
527
+ {t('resizableLabel')}
528
+ </Label>
529
+ </div>
530
+
531
+ <div className="mt-4 p-3 rounded-md bg-background border">
532
+ <p className="text-xs text-muted-foreground mb-2">
533
+ {t('sizePreview')}
534
+ </p>
535
+ <div className="grid grid-cols-12 gap-1 h-32">
536
+ {Array.from({ length: 12 }).map((_, i) => (
537
+ <div
538
+ key={i}
539
+ className={`border border-dashed ${
540
+ i < (selectedComponent.width || 3)
541
+ ? 'bg-primary/20 border-primary'
542
+ : 'border-muted'
543
+ }`}
544
+ />
545
+ ))}
546
+ </div>
547
+ <p className="text-xs text-muted-foreground mt-2">
548
+ {t('sizeLabel')} {selectedComponent.width || 3} x{' '}
549
+ {selectedComponent.height || 2}
550
+ {selectedComponent.min_width &&
551
+ ` (${t('minWidth')}: ${selectedComponent.min_width})`}
552
+ {selectedComponent.max_width &&
553
+ ` (${t('maxWidth')}: ${selectedComponent.max_width})`}
554
+ </p>
555
+ </div>
556
+ </div>
557
+ </div>
558
+ )}
559
+ <DialogFooter>
560
+ <Button onClick={handleSave}>{t('save')}</Button>
561
+ </DialogFooter>
562
+ </DialogContent>
563
+ </Dialog>
564
+ </div>
565
+
566
+ <div className="flex items-center gap-2 mb-4">
567
+ <Input
568
+ placeholder={`${t('searchPlaceholder') || 'Buscar'}...`}
569
+ value={searchQuery}
570
+ onChange={(e) => {
571
+ setSearchQuery(e.target.value);
572
+ setPage(1);
573
+ }}
574
+ className="max-w-sm"
575
+ />
576
+ </div>
577
+
578
+ <div className="rounded-md border">
579
+ <Table>
580
+ <TableHeader>
581
+ <TableRow>
582
+ <TableHead>{t('slug')}</TableHead>
583
+ <TableHead>{t('name')}</TableHead>
584
+ <TableHead>{t('descriptionLabel')}</TableHead>
585
+ <TableHead>{t('size')}</TableHead>
586
+ <TableHead>{t('resizable')}</TableHead>
587
+ <TableHead className="w-[100px]">{t('actions')}</TableHead>
588
+ </TableRow>
589
+ </TableHeader>
590
+ <TableBody>
591
+ {isLoading ? (
592
+ <TableRow>
593
+ <TableCell colSpan={7} className="text-center">
594
+ {t('loading')}
595
+ </TableCell>
596
+ </TableRow>
597
+ ) : components && components.length > 0 ? (
598
+ components.map((component) => {
599
+ const name =
600
+ component.dashboard_component_locale.find(
601
+ (l) => l.locale.code === currentLocaleCode
602
+ )?.name || component.slug;
603
+
604
+ const description = component.dashboard_component_locale.find(
605
+ (l) => l.locale.code === currentLocaleCode
606
+ )?.description;
607
+
608
+ return (
609
+ <TableRow key={component.id}>
610
+ <TableCell className="font-mono">
611
+ {component.slug}
612
+ </TableCell>
613
+ <TableCell>{name}</TableCell>
614
+ <TableCell>{description}</TableCell>
615
+ <TableCell>
616
+ {component.width}x{component.height}
617
+ </TableCell>
618
+ <TableCell>
619
+ {component.is_resizable ? t('yes') : t('no')}
620
+ </TableCell>
621
+ <TableCell>
622
+ <div className="flex gap-2">
623
+ <Button
624
+ size="icon"
625
+ variant="ghost"
626
+ onClick={() => handleEdit(component)}
627
+ >
628
+ <IconEdit className="size-4" />
629
+ </Button>
630
+ <Button
631
+ size="icon"
632
+ variant="ghost"
633
+ onClick={() => handleDeleteClick(component.id)}
634
+ >
635
+ <IconTrash className="size-4" />
636
+ </Button>
637
+ </div>
638
+ </TableCell>
639
+ </TableRow>
640
+ );
641
+ })
642
+ ) : (
643
+ <TableRow>
644
+ <TableCell colSpan={7} className="text-center">
645
+ {t('noComponents')}
646
+ </TableCell>
647
+ </TableRow>
648
+ )}
649
+ </TableBody>
650
+ </Table>
651
+ </div>
652
+
653
+ {/* Paginação */}
654
+ {totalPages > 1 && (
655
+ <div className="flex items-center justify-between px-2">
656
+ <div className="text-muted-foreground text-sm">
657
+ {t('showingResults', {
658
+ from: (page - 1) * pageSize + 1,
659
+ to: Math.min(page * pageSize, total),
660
+ total: total,
661
+ })}
662
+ </div>
663
+ <div className="flex items-center gap-2">
664
+ <div className="flex items-center gap-2">
665
+ <Label htmlFor="pageSize" className="text-sm">
666
+ {t('itemsPerPage')}
667
+ </Label>
668
+ <Select
669
+ value={String(pageSize)}
670
+ onValueChange={(value) => {
671
+ setPageSize(Number(value));
672
+ setPage(1);
673
+ }}
674
+ >
675
+ <SelectTrigger className="w-20" id="pageSize">
676
+ <SelectValue />
677
+ </SelectTrigger>
678
+ <SelectContent>
679
+ {[10, 20, 30, 50].map((size) => (
680
+ <SelectItem key={size} value={String(size)}>
681
+ {size}
682
+ </SelectItem>
683
+ ))}
684
+ </SelectContent>
685
+ </Select>
686
+ </div>
687
+ <div className="text-sm">
688
+ {t('pageOf', { page: page, total: totalPages })}
689
+ </div>
690
+ <div className="flex gap-1">
691
+ <Button
692
+ variant="outline"
693
+ size="icon"
694
+ className="size-8"
695
+ onClick={() => setPage(1)}
696
+ disabled={page === 1}
697
+ >
698
+ <IconChevronsLeft className="size-4" />
699
+ </Button>
700
+ <Button
701
+ variant="outline"
702
+ size="icon"
703
+ className="size-8"
704
+ onClick={() => setPage((p) => Math.max(1, p - 1))}
705
+ disabled={page === 1}
706
+ >
707
+ <IconChevronLeft className="size-4" />
708
+ </Button>
709
+ <Button
710
+ variant="outline"
711
+ size="icon"
712
+ className="size-8"
713
+ onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
714
+ disabled={page === totalPages}
715
+ >
716
+ <IconChevronRight className="size-4" />
717
+ </Button>
718
+ <Button
719
+ variant="outline"
720
+ size="icon"
721
+ className="size-8"
722
+ onClick={() => setPage(totalPages)}
723
+ disabled={page === totalPages}
724
+ >
725
+ <IconChevronsRight className="size-4" />
726
+ </Button>
727
+ </div>
728
+ </div>
729
+ </div>
730
+ )}
731
+
732
+ <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
733
+ <AlertDialogContent>
734
+ <AlertDialogHeader>
735
+ <AlertDialogTitle>{t('confirmDelete')}</AlertDialogTitle>
736
+ <AlertDialogDescription>
737
+ {t('deleteDescription')}
738
+ </AlertDialogDescription>
739
+ </AlertDialogHeader>
740
+ <AlertDialogFooter>
741
+ <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
742
+ <AlertDialogAction
743
+ onClick={handleDeleteConfirm}
744
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
745
+ >
746
+ {t('delete')}
747
+ </AlertDialogAction>
748
+ </AlertDialogFooter>
749
+ </AlertDialogContent>
750
+ </AlertDialog>
751
+ </div>
752
+ );
753
+ }