@hed-hog/category 0.0.361 → 0.0.364

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.
@@ -6,7 +6,6 @@ import {
6
6
  PageHeader,
7
7
  PaginationFooter,
8
8
  SearchBar,
9
- StatsCards,
10
9
  } from '@/components/entity-list';
11
10
  import {
12
11
  AlertDialog,
@@ -31,6 +30,8 @@ import {
31
30
  FormMessage,
32
31
  } from '@/components/ui/form';
33
32
  import { Input } from '@/components/ui/input';
33
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
34
+ import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
34
35
  import {
35
36
  Select,
36
37
  SelectContent,
@@ -38,7 +39,6 @@ import {
38
39
  SelectTrigger,
39
40
  SelectValue,
40
41
  } from '@/components/ui/select';
41
- import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
42
42
  import {
43
43
  Sheet,
44
44
  SheetDescription,
@@ -46,6 +46,15 @@ import {
46
46
  SheetHeader,
47
47
  SheetTitle,
48
48
  } from '@/components/ui/sheet';
49
+ import {
50
+ Table,
51
+ TableBody,
52
+ TableCell,
53
+ TableHead,
54
+ TableHeader,
55
+ TableRow,
56
+ } from '@/components/ui/table';
57
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
49
58
  import { useDebounce } from '@/hooks/use-debounce';
50
59
  import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
51
60
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
@@ -56,6 +65,8 @@ import {
56
65
  FolderTree,
57
66
  Globe,
58
67
  Layers,
68
+ LayoutGrid,
69
+ List,
59
70
  Plus,
60
71
  Save,
61
72
  Tag,
@@ -105,6 +116,9 @@ type CategoryFormValues = {
105
116
  status: 'active' | 'inactive';
106
117
  };
107
118
 
119
+ const CATEGORY_VIEW_STORAGE_KEY = 'category-view-mode';
120
+ type CategoryViewMode = 'table' | 'cards';
121
+
108
122
  export default function CategoryPage() {
109
123
  const t = useTranslations('category.Category');
110
124
  const [editingCategoryId, setEditingCategoryId] = useState<number | null>(
@@ -123,6 +137,7 @@ export default function CategoryPage() {
123
137
  defaultValue: 10,
124
138
  allowedValues: [10, 20, 30, 40, 50],
125
139
  });
140
+ const [viewMode, setViewMode] = useState<CategoryViewMode>('table');
126
141
  const [statusFilter, setStatusFilter] = useState<string>('all');
127
142
  const [parentFilter, setParentFilter] = useState<string>('all');
128
143
  const [selectedLocale, setSelectedLocale] = useState<string>('');
@@ -175,6 +190,17 @@ export default function CategoryPage() {
175
190
  }
176
191
  }, [currentLocaleCode]);
177
192
 
193
+ useEffect(() => {
194
+ try {
195
+ const saved = window.localStorage.getItem(CATEGORY_VIEW_STORAGE_KEY);
196
+ if (saved === 'table' || saved === 'cards') {
197
+ setViewMode(saved);
198
+ }
199
+ } catch {
200
+ // Ignore storage read failures.
201
+ }
202
+ }, []);
203
+
178
204
  const { data: categoriesResult, refetch: refetchCategories } = useQuery<
179
205
  PaginationResult<Category>
180
206
  >({
@@ -275,7 +301,7 @@ export default function CategoryPage() {
275
301
  const handleEditCategory = async (category: Category): Promise<void> => {
276
302
  try {
277
303
  const response = await request({
278
- url: `/category/${category.category_id}`,
304
+ url: `/category/${category.id}`,
279
305
  method: 'GET',
280
306
  });
281
307
 
@@ -402,6 +428,19 @@ export default function CategoryPage() {
402
428
  setPage(1);
403
429
  };
404
430
 
431
+ const handleViewModeChange = (value: string): void => {
432
+ if (value !== 'table' && value !== 'cards') {
433
+ return;
434
+ }
435
+
436
+ setViewMode(value);
437
+ try {
438
+ window.localStorage.setItem(CATEGORY_VIEW_STORAGE_KEY, value);
439
+ } catch {
440
+ // Ignore storage write failures.
441
+ }
442
+ };
443
+
405
444
  useEffect(() => {
406
445
  if (!isEditDialogOpen || !selectedLocale) {
407
446
  return;
@@ -422,35 +461,83 @@ export default function CategoryPage() {
422
461
 
423
462
  const statsCards = [
424
463
  {
464
+ key: 'total',
425
465
  title: t('totalCategories'),
426
466
  value: statsData?.total || 0,
427
- icon: <Layers className="h-5 w-5" />,
428
- iconBgColor: 'bg-blue-100',
429
- iconColor: 'text-blue-600',
467
+ icon: Layers,
468
+ accentClassName: 'from-sky-500/20 via-blue-400/10 to-transparent',
469
+ iconContainerClassName: 'bg-sky-50 text-sky-600',
430
470
  },
431
471
  {
472
+ key: 'active',
432
473
  title: t('actives'),
433
474
  value: statsData?.totalActive || 0,
434
- icon: <Tag className="h-5 w-5" />,
435
- iconBgColor: 'bg-green-100',
436
- iconColor: 'text-green-600',
475
+ icon: Tag,
476
+ accentClassName: 'from-emerald-500/20 via-green-400/10 to-transparent',
477
+ iconContainerClassName: 'bg-emerald-50 text-emerald-600',
437
478
  },
438
479
  {
480
+ key: 'inactive',
439
481
  title: t('inactives'),
440
482
  value: statsData?.totalInactive || 0,
441
- icon: <Tag className="h-5 w-5" />,
442
- iconBgColor: 'bg-orange-100',
443
- iconColor: 'text-orange-600',
483
+ icon: Tag,
484
+ accentClassName: 'from-amber-500/20 via-orange-400/10 to-transparent',
485
+ iconContainerClassName: 'bg-amber-50 text-amber-600',
444
486
  },
445
487
  {
488
+ key: 'root',
446
489
  title: t('root'),
447
490
  value: statsData?.totalRoot || 0,
448
- icon: <FolderTree className="h-5 w-5" />,
449
- iconBgColor: 'bg-purple-100',
450
- iconColor: 'text-purple-600',
491
+ icon: FolderTree,
492
+ accentClassName: 'from-violet-500/20 via-purple-400/10 to-transparent',
493
+ iconContainerClassName: 'bg-violet-50 text-violet-600',
451
494
  },
452
495
  ];
453
496
 
497
+ const renderCategoryActions = (category: Category) => (
498
+ <div className="flex items-center justify-end gap-2">
499
+ <Button
500
+ variant="outline"
501
+ size="sm"
502
+ onClick={() => handleEditCategory(category)}
503
+ className="transition-colors hover:border-blue-200 hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-950"
504
+ >
505
+ <Edit className="mr-1 h-4 w-4" />
506
+ {t('edit')}
507
+ </Button>
508
+
509
+ <AlertDialog>
510
+ <AlertDialogTrigger asChild>
511
+ <Button
512
+ variant="outline"
513
+ size="sm"
514
+ className="bg-transparent transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950"
515
+ >
516
+ <Trash2 className="mr-1 h-4 w-4" />
517
+ {t('delete')}
518
+ </Button>
519
+ </AlertDialogTrigger>
520
+ <AlertDialogContent>
521
+ <AlertDialogHeader>
522
+ <AlertDialogTitle>{t('confirmDelete')}</AlertDialogTitle>
523
+ <AlertDialogDescription>
524
+ {t('deleteDescription')}
525
+ </AlertDialogDescription>
526
+ </AlertDialogHeader>
527
+ <AlertDialogFooter>
528
+ <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
529
+ <AlertDialogAction
530
+ onClick={() => handleDeleteCategory(category.id)}
531
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
532
+ >
533
+ {t('delete')}
534
+ </AlertDialogAction>
535
+ </AlertDialogFooter>
536
+ </AlertDialogContent>
537
+ </AlertDialog>
538
+ </div>
539
+ );
540
+
454
541
  return (
455
542
  <Page>
456
543
  <PageHeader
@@ -466,154 +553,229 @@ export default function CategoryPage() {
466
553
  description={t('description')}
467
554
  />
468
555
 
469
- <StatsCards stats={statsCards} />
556
+ <KpiCardsGrid items={statsCards} />
470
557
 
471
- <SearchBar
472
- searchQuery={searchTerm}
473
- onSearchChange={handleSearchChange}
474
- onSearch={() => setPage(1)}
475
- placeholder={t('searchPlaceholder')}
476
- controls={[
477
- {
478
- id: 'status-filter',
479
- type: 'select',
480
- value: statusFilter,
481
- onChange: setStatusFilter,
482
- placeholder: t('status'),
483
- options: [
484
- { value: 'all', label: t('all') },
485
- { value: 'active', label: t('actives') },
486
- { value: 'inactive', label: t('inactives') },
487
- ],
488
- },
489
- {
490
- id: 'parent-filter',
491
- type: 'select',
492
- value: parentFilter,
493
- onChange: setParentFilter,
494
- placeholder: t('hierarchy'),
495
- options: [
496
- { value: 'all', label: t('allHierarchy') },
497
- { value: 'root', label: t('rootHierarchy') },
498
- ],
499
- },
500
- ]}
501
- />
558
+ <div className="flex flex-col gap-4 xl:flex-row xl:items-center">
559
+ <div className="flex-1">
560
+ <SearchBar
561
+ searchQuery={searchTerm}
562
+ onSearchChange={handleSearchChange}
563
+ onSearch={() => setPage(1)}
564
+ placeholder={t('searchPlaceholder')}
565
+ controls={[
566
+ {
567
+ id: 'status-filter',
568
+ type: 'select',
569
+ value: statusFilter,
570
+ onChange: (value) => {
571
+ setStatusFilter(value);
572
+ setPage(1);
573
+ },
574
+ placeholder: t('status'),
575
+ options: [
576
+ { value: 'all', label: t('all') },
577
+ { value: 'active', label: t('actives') },
578
+ { value: 'inactive', label: t('inactives') },
579
+ ],
580
+ },
581
+ {
582
+ id: 'parent-filter',
583
+ type: 'select',
584
+ value: parentFilter,
585
+ onChange: (value) => {
586
+ setParentFilter(value);
587
+ setPage(1);
588
+ },
589
+ placeholder: t('hierarchy'),
590
+ options: [
591
+ { value: 'all', label: t('allHierarchy') },
592
+ { value: 'root', label: t('rootHierarchy') },
593
+ ],
594
+ },
595
+ ]}
596
+ />
597
+ </div>
598
+
599
+ <div className="flex items-center gap-3 xl:justify-end">
600
+ <span className="text-xs font-medium text-muted-foreground">
601
+ {t('viewMode')}
602
+ </span>
603
+ <ToggleGroup
604
+ type="single"
605
+ value={viewMode}
606
+ onValueChange={handleViewModeChange}
607
+ variant="outline"
608
+ size="sm"
609
+ aria-label={t('viewMode')}
610
+ >
611
+ <ToggleGroupItem
612
+ value="table"
613
+ className="gap-1.5 px-2.5"
614
+ aria-label={t('viewModeTable')}
615
+ >
616
+ <List className="h-4 w-4" />
617
+ <span className="hidden sm:inline">{t('viewModeTable')}</span>
618
+ </ToggleGroupItem>
619
+ <ToggleGroupItem
620
+ value="cards"
621
+ className="gap-1.5 px-2.5"
622
+ aria-label={t('viewModeCards')}
623
+ >
624
+ <LayoutGrid className="h-4 w-4" />
625
+ <span className="hidden sm:inline">{t('viewModeCards')}</span>
626
+ </ToggleGroupItem>
627
+ </ToggleGroup>
628
+ </div>
629
+ </div>
502
630
 
503
631
  <div className="space-y-4">
504
632
  {categories.length > 0 ? (
505
- <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
506
- {categories.map((category) => (
507
- <Card
508
- key={category.id}
509
- onDoubleClick={() => handleEditCategory(category)}
510
- className="cursor-pointer transition-all duration-200 hover:border-primary/20 hover:shadow-md"
511
- >
512
- <CardContent className="p-4">
513
- <div className="flex items-start justify-between gap-3">
514
- <div className="flex min-w-0 items-start gap-3">
515
- <div
516
- className="mt-0.5 rounded-full p-2"
517
- style={{
518
- backgroundColor: category.color
519
- ? `${category.color}20`
520
- : '#00000020',
521
- }}
522
- >
523
- <div style={{ color: category.color || '#000000' }}>
524
- {renderIcon(category.icon)}
633
+ viewMode === 'table' ? (
634
+ <div className="overflow-x-auto">
635
+ <Table>
636
+ <TableHeader>
637
+ <TableRow>
638
+ <TableHead>{t('name')}</TableHead>
639
+ <TableHead>{t('slug')}</TableHead>
640
+ <TableHead>{t('color')}</TableHead>
641
+ <TableHead>{t('icon')}</TableHead>
642
+ <TableHead>{t('status')}</TableHead>
643
+ <TableHead className="text-right">{t('edit')}</TableHead>
644
+ </TableRow>
645
+ </TableHeader>
646
+ <TableBody>
647
+ {categories.map((category) => (
648
+ <TableRow
649
+ key={category.id}
650
+ onDoubleClick={() => handleEditCategory(category)}
651
+ className="cursor-pointer"
652
+ >
653
+ <TableCell>
654
+ <div className="flex min-w-0 items-center gap-3">
655
+ <div
656
+ className="rounded-full p-2"
657
+ style={{
658
+ backgroundColor: category.color
659
+ ? `${category.color}20`
660
+ : '#00000020',
661
+ }}
662
+ >
663
+ <div style={{ color: category.color || '#000000' }}>
664
+ {renderIcon(category.icon)}
665
+ </div>
666
+ </div>
667
+ <div className="min-w-0">
668
+ <div className="truncate font-medium">
669
+ {category.name}
670
+ </div>
671
+ <div className="text-xs text-muted-foreground">
672
+ {category.category_id
673
+ ? t('hierarchy')
674
+ : t('root')}
675
+ </div>
676
+ </div>
525
677
  </div>
526
- </div>
527
- <div className="min-w-0 space-y-2">
528
- <div className="flex items-center gap-2 flex-wrap">
529
- <h3 className="truncate text-base font-semibold leading-tight">
530
- {category.name}
531
- </h3>
532
- {getStatusBadge(category.status)}
678
+ </TableCell>
679
+ <TableCell className="font-mono text-sm">
680
+ {category.slug}
681
+ </TableCell>
682
+ <TableCell>
683
+ {category.color ? (
684
+ <div className="flex items-center gap-2">
685
+ <span
686
+ className="inline-block h-3.5 w-3.5 rounded border"
687
+ style={{ backgroundColor: category.color }}
688
+ />
689
+ <span>{category.color}</span>
690
+ </div>
691
+ ) : (
692
+ '—'
693
+ )}
694
+ </TableCell>
695
+ <TableCell>{category.icon || '—'}</TableCell>
696
+ <TableCell>{getStatusBadge(category.status)}</TableCell>
697
+ <TableCell className="text-right">
698
+ {renderCategoryActions(category)}
699
+ </TableCell>
700
+ </TableRow>
701
+ ))}
702
+ </TableBody>
703
+ </Table>
704
+ </div>
705
+ ) : (
706
+ <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
707
+ {categories.map((category) => (
708
+ <Card
709
+ key={category.id}
710
+ onDoubleClick={() => handleEditCategory(category)}
711
+ className="cursor-pointer transition-all duration-200 hover:border-primary/20 hover:shadow-md"
712
+ >
713
+ <CardContent className="p-4">
714
+ <div className="flex items-start justify-between gap-3">
715
+ <div className="flex min-w-0 items-start gap-3">
716
+ <div
717
+ className="mt-0.5 rounded-full p-2"
718
+ style={{
719
+ backgroundColor: category.color
720
+ ? `${category.color}20`
721
+ : '#00000020',
722
+ }}
723
+ >
724
+ <div style={{ color: category.color || '#000000' }}>
725
+ {renderIcon(category.icon)}
726
+ </div>
533
727
  </div>
534
- <div className="space-y-1 text-xs text-muted-foreground">
535
- <p className="truncate">
536
- <span className="font-medium text-foreground">
537
- {t('slug')}:
538
- </span>{' '}
539
- {category.slug}
540
- </p>
541
- {category.color && (
542
- <p className="flex items-center gap-2">
543
- <span className="font-medium text-foreground">
544
- {t('color')}:
545
- </span>
546
- <span
547
- className="inline-block h-3.5 w-3.5 rounded border"
548
- style={{
549
- backgroundColor: category.color,
550
- }}
551
- />
552
- <span className="truncate">{category.color}</span>
553
- </p>
554
- )}
555
- {category.icon && (
728
+ <div className="min-w-0 space-y-2">
729
+ <div className="flex flex-wrap items-center gap-2">
730
+ <h3 className="truncate text-base font-semibold leading-tight">
731
+ {category.name}
732
+ </h3>
733
+ {getStatusBadge(category.status)}
734
+ </div>
735
+ <div className="space-y-1 text-xs text-muted-foreground">
556
736
  <p className="truncate">
557
737
  <span className="font-medium text-foreground">
558
- {t('icon')}:
738
+ {t('slug')}:
559
739
  </span>{' '}
560
- {category.icon}
740
+ {category.slug}
561
741
  </p>
562
- )}
742
+ {category.color && (
743
+ <p className="flex items-center gap-2">
744
+ <span className="font-medium text-foreground">
745
+ {t('color')}:
746
+ </span>
747
+ <span
748
+ className="inline-block h-3.5 w-3.5 rounded border"
749
+ style={{
750
+ backgroundColor: category.color,
751
+ }}
752
+ />
753
+ <span className="truncate">
754
+ {category.color}
755
+ </span>
756
+ </p>
757
+ )}
758
+ {category.icon && (
759
+ <p className="truncate">
760
+ <span className="font-medium text-foreground">
761
+ {t('icon')}:
762
+ </span>{' '}
763
+ {category.icon}
764
+ </p>
765
+ )}
766
+ </div>
563
767
  </div>
564
768
  </div>
565
769
  </div>
566
- </div>
567
-
568
- <div className="mt-4 flex items-center justify-end gap-2">
569
- <Button
570
- variant="outline"
571
- size="sm"
572
- onClick={() => handleEditCategory(category)}
573
- className="transition-colors hover:border-blue-200 hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-950"
574
- >
575
- <Edit className="mr-1 h-4 w-4" />
576
- {t('edit')}
577
- </Button>
578
-
579
- <AlertDialog>
580
- <AlertDialogTrigger asChild>
581
- <Button
582
- variant="outline"
583
- size="sm"
584
- className="bg-transparent transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950"
585
- >
586
- <Trash2 className="mr-1 h-4 w-4" />
587
- {t('delete')}
588
- </Button>
589
- </AlertDialogTrigger>
590
- <AlertDialogContent>
591
- <AlertDialogHeader>
592
- <AlertDialogTitle>
593
- {t('confirmDelete')}
594
- </AlertDialogTitle>
595
- <AlertDialogDescription>
596
- {t('deleteDescription')}
597
- </AlertDialogDescription>
598
- </AlertDialogHeader>
599
- <AlertDialogFooter>
600
- <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
601
- <AlertDialogAction
602
- onClick={() =>
603
- handleDeleteCategory(Number(category.category_id))
604
- }
605
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
606
- >
607
- {t('delete')}
608
- </AlertDialogAction>
609
- </AlertDialogFooter>
610
- </AlertDialogContent>
611
- </AlertDialog>
612
- </div>
613
- </CardContent>
614
- </Card>
615
- ))}
616
- </div>
770
+
771
+ <div className="mt-4">
772
+ {renderCategoryActions(category)}
773
+ </div>
774
+ </CardContent>
775
+ </Card>
776
+ ))}
777
+ </div>
778
+ )
617
779
  ) : (
618
780
  <EmptyState
619
781
  icon={<Layers className="h-12 w-12" />}
@@ -818,10 +980,7 @@ export default function CategoryPage() {
818
980
  <SelectItem value="none">{t('noneRoot')}</SelectItem>
819
981
  {Array.isArray(rootCategories) &&
820
982
  rootCategories.map((cat: Category) => (
821
- <SelectItem
822
- key={cat.id}
823
- value={String(cat.category_id)}
824
- >
983
+ <SelectItem key={cat.id} value={String(cat.id)}>
825
984
  {cat.name}
826
985
  </SelectItem>
827
986
  ))}
@@ -10,6 +10,9 @@
10
10
  "root": "Root",
11
11
  "searchPlaceholder": "Search by name or slug...",
12
12
  "status": "Status",
13
+ "viewMode": "View",
14
+ "viewModeTable": "Table",
15
+ "viewModeCards": "Grid",
13
16
  "all": "All",
14
17
  "actives": "Active",
15
18
  "inactives": "Inactive",
@@ -10,6 +10,9 @@
10
10
  "root": "Raiz",
11
11
  "searchPlaceholder": "Buscar por nome ou slug...",
12
12
  "status": "Status",
13
+ "viewMode": "Visualização",
14
+ "viewModeTable": "Tabela",
15
+ "viewModeCards": "Grade",
13
16
  "all": "Todos",
14
17
  "actives": "Ativos",
15
18
  "inactives": "Inativos",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/category",
3
- "version": "0.0.361",
3
+ "version": "0.0.364",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -9,11 +9,11 @@
9
9
  "@nestjs/core": "^11",
10
10
  "@nestjs/jwt": "^11",
11
11
  "@nestjs/mapped-types": "*",
12
- "@hed-hog/core": "0.0.361",
13
12
  "@hed-hog/api-locale": "0.0.14",
14
- "@hed-hog/api-prisma": "0.0.6",
15
13
  "@hed-hog/api": "0.0.8",
16
- "@hed-hog/api-pagination": "0.0.7"
14
+ "@hed-hog/api-prisma": "0.0.6",
15
+ "@hed-hog/api-pagination": "0.0.7",
16
+ "@hed-hog/core": "0.0.364"
17
17
  },
18
18
  "exports": {
19
19
  ".": {