@hed-hog/category 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 CHANGED
@@ -1,3 +1,4 @@
1
+ ```markdown
1
2
  # @hed-hog/category
2
3
 
3
4
  ## 1. Visão geral do módulo
@@ -322,3 +323,4 @@ Authorization: Bearer <token>
322
323
  ---
323
324
 
324
325
  Este módulo é essencial para a organização e categorização dos conteúdos e produtos no sistema HedHog, garantindo flexibilidade e suporte multilíngue com controle de acesso robusto.
326
+ ```
@@ -1,9 +1,12 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
+ EmptyState,
5
+ Page,
4
6
  PageHeader,
5
7
  PaginationFooter,
6
8
  SearchBar,
9
+ StatsCards,
7
10
  } from '@/components/entity-list';
8
11
  import {
9
12
  AlertDialog,
@@ -20,15 +23,14 @@ import { Badge } from '@/components/ui/badge';
20
23
  import { Button } from '@/components/ui/button';
21
24
  import { Card, CardContent } from '@/components/ui/card';
22
25
  import {
23
- Dialog,
24
- DialogContent,
25
- DialogDescription,
26
- DialogFooter,
27
- DialogHeader,
28
- DialogTitle,
29
- } from '@/components/ui/dialog';
26
+ Form,
27
+ FormControl,
28
+ FormField,
29
+ FormItem,
30
+ FormLabel,
31
+ FormMessage,
32
+ } from '@/components/ui/form';
30
33
  import { Input } from '@/components/ui/input';
31
- import { Label } from '@/components/ui/label';
32
34
  import {
33
35
  Select,
34
36
  SelectContent,
@@ -36,8 +38,17 @@ import {
36
38
  SelectTrigger,
37
39
  SelectValue,
38
40
  } from '@/components/ui/select';
41
+ import {
42
+ Sheet,
43
+ SheetContent,
44
+ SheetDescription,
45
+ SheetFooter,
46
+ SheetHeader,
47
+ SheetTitle,
48
+ } from '@/components/ui/sheet';
39
49
  import { useDebounce } from '@/hooks/use-debounce';
40
50
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
51
+ import { zodResolver } from '@hookform/resolvers/zod';
41
52
  import * as TablerIcons from '@tabler/icons-react';
42
53
  import {
43
54
  Edit,
@@ -48,11 +59,12 @@ import {
48
59
  Save,
49
60
  Tag,
50
61
  Trash2,
51
- X,
52
62
  } from 'lucide-react';
53
63
  import { useTranslations } from 'next-intl';
54
64
  import { useEffect, useState } from 'react';
65
+ import { useForm } from 'react-hook-form';
55
66
  import { toast } from 'sonner';
67
+ import { z } from 'zod';
56
68
 
57
69
  type PaginationResult<T> = {
58
70
  data: T[];
@@ -83,10 +95,23 @@ type Locale = {
83
95
  name: string;
84
96
  };
85
97
 
98
+ type CategoryFormValues = {
99
+ name: string;
100
+ slug: string;
101
+ color: string;
102
+ icon: string;
103
+ category_id: string;
104
+ status: 'active' | 'inactive';
105
+ };
106
+
86
107
  export default function CategoryPage() {
87
108
  const t = useTranslations('category.Category');
88
- const [categories, setCategories] = useState<Category[]>([]);
89
- const [selectedCategory, setSelectedCategory] = useState<any | null>(null);
109
+ const [editingCategoryId, setEditingCategoryId] = useState<number | null>(
110
+ null
111
+ );
112
+ const [localeData, setLocaleData] = useState<
113
+ Record<string, { name: string }>
114
+ >({});
90
115
  const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
91
116
  const [isNewCategory, setIsNewCategory] = useState(false);
92
117
  const [searchTerm, setSearchTerm] = useState('');
@@ -99,6 +124,28 @@ export default function CategoryPage() {
99
124
  const [fullLocales, setFullLocales] = useState<any[]>([]);
100
125
  const { request, locales, currentLocaleCode } = useApp();
101
126
 
127
+ const categorySchema = z.object({
128
+ name: z.string().trim().min(1, t('nameRequired')),
129
+ slug: z.string().trim().min(1, t('slugRequired')),
130
+ color: z.string().trim().min(1, t('color')),
131
+ icon: z.string().trim().optional(),
132
+ category_id: z.string(),
133
+ status: z.enum(['active', 'inactive']),
134
+ });
135
+
136
+ const form = useForm<CategoryFormValues>({
137
+ resolver: zodResolver(categorySchema),
138
+ mode: 'onChange',
139
+ defaultValues: {
140
+ name: '',
141
+ slug: '',
142
+ color: '#000000',
143
+ icon: '',
144
+ category_id: 'none',
145
+ status: 'active',
146
+ },
147
+ });
148
+
102
149
  // Buscar locales completos com id
103
150
  useEffect(() => {
104
151
  const fetchLocales = async () => {
@@ -123,9 +170,9 @@ export default function CategoryPage() {
123
170
  }
124
171
  }, [currentLocaleCode]);
125
172
 
126
- const { data: categoriesResult, refetch: refetchCategories } = useQuery<
127
- PaginationResult<Category>
128
- >({
173
+ const { data: categoriesResult, refetch: refetchCategories } = useQuery<
174
+ PaginationResult<Category>
175
+ >({
129
176
  queryKey: [
130
177
  'categories',
131
178
  debouncedSearch,
@@ -147,10 +194,10 @@ export default function CategoryPage() {
147
194
  });
148
195
  return response.data as PaginationResult<Category>;
149
196
  },
150
- });
151
- const { data = [], total = 0 } = categoriesResult ?? {};
197
+ });
198
+ const { data: categories = [], total = 0 } = categoriesResult ?? {};
152
199
 
153
- const { data: rootCategories = [] } = useQuery<Category[]>({
200
+ const { data: rootCategories = [] } = useQuery<Category[]>({
154
201
  queryKey: ['root-categories'],
155
202
  queryFn: async () => {
156
203
  const response = await request({
@@ -158,9 +205,9 @@ export default function CategoryPage() {
158
205
  });
159
206
  return (response.data || []) as Category[];
160
207
  },
161
- enabled: true,
162
- staleTime: 0,
163
- refetchOnMount: true,
208
+ enabled: true,
209
+ staleTime: 0,
210
+ refetchOnMount: true,
164
211
  });
165
212
 
166
213
  const { data: statsData, refetch: refetchStats } = useQuery<any>({
@@ -173,12 +220,6 @@ export default function CategoryPage() {
173
220
  },
174
221
  });
175
222
 
176
- useEffect(() => {
177
- if (data) {
178
- setCategories(data);
179
- }
180
- }, [data]);
181
-
182
223
  const renderIcon = (iconName?: string) => {
183
224
  const toPascalCase = (str: string) =>
184
225
  str.replace(/(^\w|-\w)/g, (match) =>
@@ -202,22 +243,26 @@ export default function CategoryPage() {
202
243
  };
203
244
 
204
245
  const handleNewCategory = (): void => {
205
- const newCategory: any = {
206
- slug: '',
207
- category_id: null,
208
- color: '#000000',
209
- icon: '',
210
- status: 'active',
211
- locale: {},
212
- };
213
-
246
+ const nextLocaleData: Record<string, { name: string }> = {};
214
247
  locales.forEach((locale: Locale) => {
215
- newCategory.locale[locale.code] = {
248
+ nextLocaleData[locale.code] = {
216
249
  name: '',
217
250
  };
218
251
  });
219
252
 
220
- setSelectedCategory(newCategory);
253
+ const initialLocale = currentLocaleCode || locales[0]?.code || '';
254
+
255
+ setEditingCategoryId(null);
256
+ setLocaleData(nextLocaleData);
257
+ setSelectedLocale(initialLocale);
258
+ form.reset({
259
+ name: '',
260
+ slug: '',
261
+ color: '#000000',
262
+ icon: '',
263
+ category_id: 'none',
264
+ status: 'active',
265
+ });
221
266
  setIsNewCategory(true);
222
267
  setIsEditDialogOpen(true);
223
268
  };
@@ -261,9 +306,21 @@ export default function CategoryPage() {
261
306
  }
262
307
  });
263
308
 
264
- setSelectedCategory({
265
- ...categoryData,
266
- locale: localeData,
309
+ const initialLocale =
310
+ selectedLocale || currentLocaleCode || locales[0]?.code || '';
311
+
312
+ setEditingCategoryId(Number(categoryData.id));
313
+ setLocaleData(localeData);
314
+ setSelectedLocale(initialLocale);
315
+ form.reset({
316
+ name: localeData[initialLocale]?.name || '',
317
+ slug: categoryData.slug || '',
318
+ color: categoryData.color || '#000000',
319
+ icon: categoryData.icon || '',
320
+ category_id: categoryData.category_id
321
+ ? String(categoryData.category_id)
322
+ : 'none',
323
+ status: categoryData.status === 'inactive' ? 'inactive' : 'active',
267
324
  });
268
325
  setIsNewCategory(false);
269
326
  setIsEditDialogOpen(true);
@@ -273,22 +330,28 @@ export default function CategoryPage() {
273
330
  }
274
331
  };
275
332
 
276
- const handleSaveCategory = async () => {
277
- if (!selectedCategory) return;
333
+ const handleSaveCategory = form.handleSubmit(async (values) => {
334
+ const mergedLocaleData = {
335
+ ...localeData,
336
+ [selectedLocale]: {
337
+ name: values.name,
338
+ },
339
+ };
278
340
 
279
341
  const payload = {
280
- locale: selectedCategory.locale,
281
- slug: selectedCategory.slug,
282
- category_id: selectedCategory.category_id || null,
283
- color: selectedCategory.color,
284
- icon: selectedCategory.icon,
285
- status: selectedCategory.status,
342
+ locale: mergedLocaleData,
343
+ slug: values.slug,
344
+ category_id:
345
+ values.category_id === 'none' ? null : Number(values.category_id),
346
+ color: values.color,
347
+ icon: values.icon,
348
+ status: values.status,
286
349
  };
287
350
 
288
351
  try {
289
- if (selectedCategory.id) {
352
+ if (editingCategoryId) {
290
353
  await request({
291
- url: `/category/${selectedCategory.id}`,
354
+ url: `/category/${editingCategoryId}`,
292
355
  method: 'PATCH',
293
356
  data: payload,
294
357
  });
@@ -303,13 +366,16 @@ export default function CategoryPage() {
303
366
  }
304
367
 
305
368
  setIsEditDialogOpen(false);
369
+ setEditingCategoryId(null);
370
+ setLocaleData({});
371
+ form.reset();
306
372
  await refetchCategories();
307
373
  await refetchStats();
308
374
  } catch (error) {
309
375
  console.error(error);
310
376
  toast.error(t('errorSave'));
311
377
  }
312
- };
378
+ });
313
379
 
314
380
  const handleDeleteCategory = async (categoryId: number): Promise<void> => {
315
381
  try {
@@ -328,19 +394,16 @@ export default function CategoryPage() {
328
394
 
329
395
  const handleSearchChange = (value: string): void => {
330
396
  setSearchTerm(value);
397
+ setPage(1);
331
398
  };
332
399
 
333
400
  useEffect(() => {
334
- refetchCategories();
335
- refetchStats();
336
- }, [
337
- isEditDialogOpen,
338
- debouncedSearch,
339
- page,
340
- pageSize,
341
- statusFilter,
342
- parentFilter,
343
- ]);
401
+ if (!isEditDialogOpen || !selectedLocale) {
402
+ return;
403
+ }
404
+
405
+ form.setValue('name', localeData[selectedLocale]?.name || '');
406
+ }, [form, isEditDialogOpen, localeData, selectedLocale]);
344
407
 
345
408
  const getStatusBadge = (status: string) => {
346
409
  return status === 'active' ? (
@@ -352,13 +415,41 @@ export default function CategoryPage() {
352
415
  );
353
416
  };
354
417
 
418
+ const statsCards = [
419
+ {
420
+ title: t('totalCategories'),
421
+ value: statsData?.total || 0,
422
+ icon: <Layers className="h-5 w-5" />,
423
+ iconBgColor: 'bg-blue-100',
424
+ iconColor: 'text-blue-600',
425
+ },
426
+ {
427
+ title: t('actives'),
428
+ value: statsData?.totalActive || 0,
429
+ icon: <Tag className="h-5 w-5" />,
430
+ iconBgColor: 'bg-green-100',
431
+ iconColor: 'text-green-600',
432
+ },
433
+ {
434
+ title: t('inactives'),
435
+ value: statsData?.totalInactive || 0,
436
+ icon: <Tag className="h-5 w-5" />,
437
+ iconBgColor: 'bg-orange-100',
438
+ iconColor: 'text-orange-600',
439
+ },
440
+ {
441
+ title: t('root'),
442
+ value: statsData?.totalRoot || 0,
443
+ icon: <FolderTree className="h-5 w-5" />,
444
+ iconBgColor: 'bg-purple-100',
445
+ iconColor: 'text-purple-600',
446
+ },
447
+ ];
448
+
355
449
  return (
356
- <div className="flex flex-col h-screen px-4">
450
+ <Page>
357
451
  <PageHeader
358
- breadcrumbs={[
359
- { label: 'Home', href: '/' },
360
- { label: t('description') },
361
- ]}
452
+ breadcrumbs={[{ label: 'Home', href: '/' }, { label: t('title') }]}
362
453
  actions={[
363
454
  {
364
455
  label: t('newCategory'),
@@ -370,235 +461,163 @@ export default function CategoryPage() {
370
461
  description={t('description')}
371
462
  />
372
463
 
373
- <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
374
- <Card className="transition-shadow hover:shadow-md p-0">
375
- <CardContent className="p-4">
376
- <div className="flex items-center space-x-3">
377
- <div className="rounded-full bg-blue-100 p-2 dark:bg-blue-900">
378
- <Layers className="h-6 w-6 text-blue-600 dark:text-blue-400" />
379
- </div>
380
- <div>
381
- <p className="text-sm font-medium text-muted-foreground">
382
- {t('totalCategories')}
383
- </p>
384
- <p className="text-2xl font-bold">{statsData?.total || 0}</p>
385
- </div>
386
- </div>
387
- </CardContent>
388
- </Card>
389
-
390
- <Card className="transition-shadow hover:shadow-md p-0">
391
- <CardContent className="p-4">
392
- <div className="flex items-center space-x-3">
393
- <div className="rounded-full bg-green-100 p-2 dark:bg-green-900">
394
- <Tag className="h-6 w-6 text-green-600 dark:text-green-400" />
395
- </div>
396
- <div>
397
- <p className="text-sm font-medium text-muted-foreground">
398
- {t('actives')}
399
- </p>
400
- <p className="text-2xl font-bold">
401
- {statsData?.totalActive || 0}
402
- </p>
403
- </div>
404
- </div>
405
- </CardContent>
406
- </Card>
407
-
408
- <Card className="transition-shadow hover:shadow-md p-0">
409
- <CardContent className="p-4">
410
- <div className="flex items-center space-x-3">
411
- <div className="rounded-full bg-orange-100 p-2 dark:bg-orange-900">
412
- <Tag className="h-6 w-6 text-orange-600 dark:text-orange-400" />
413
- </div>
414
- <div>
415
- <p className="text-sm font-medium text-muted-foreground">
416
- {t('inactives')}
417
- </p>
418
- <p className="text-2xl font-bold">
419
- {statsData?.totalInactive || 0}
420
- </p>
421
- </div>
422
- </div>
423
- </CardContent>
424
- </Card>
425
-
426
- <Card className="transition-shadow hover:shadow-md p-0">
427
- <CardContent className="p-4">
428
- <div className="flex items-center space-x-3">
429
- <div className="rounded-full bg-purple-100 p-2 dark:bg-purple-900">
430
- <FolderTree className="h-6 w-6 text-purple-600 dark:text-purple-400" />
431
- </div>
432
- <div>
433
- <p className="text-sm font-medium text-muted-foreground">
434
- {t('root')}
435
- </p>
436
- <p className="text-2xl font-bold">
437
- {statsData?.totalRoot || 0}
438
- </p>
439
- </div>
440
- </div>
441
- </CardContent>
442
- </Card>
443
- </div>
444
-
445
- <div className="flex flex-col gap-4 sm:flex-row my-4">
446
- <SearchBar
447
- searchQuery={searchTerm}
448
- onSearchChange={handleSearchChange}
449
- onSearch={() => refetchCategories()}
450
- placeholder={t('searchPlaceholder')}
451
- />
464
+ <StatsCards stats={statsCards} />
452
465
 
453
- <Select value={statusFilter} onValueChange={setStatusFilter}>
454
- <SelectTrigger className="w-full sm:w-[180px]">
455
- <SelectValue placeholder={t('status')} />
456
- </SelectTrigger>
457
- <SelectContent>
458
- <SelectItem value="all">{t('all')}</SelectItem>
459
- <SelectItem value="active">{t('actives')}</SelectItem>
460
- <SelectItem value="inactive">{t('inactives')}</SelectItem>
461
- </SelectContent>
462
- </Select>
463
- <Select value={parentFilter} onValueChange={setParentFilter}>
464
- <SelectTrigger className="w-full sm:w-[180px]">
465
- <SelectValue placeholder={t('hierarchy')} />
466
- </SelectTrigger>
467
- <SelectContent>
468
- <SelectItem value="all">{t('allHierarchy')}</SelectItem>
469
- <SelectItem value="root">{t('rootHierarchy')}</SelectItem>
470
- </SelectContent>
471
- </Select>
472
- </div>
466
+ <SearchBar
467
+ searchQuery={searchTerm}
468
+ onSearchChange={handleSearchChange}
469
+ onSearch={() => setPage(1)}
470
+ placeholder={t('searchPlaceholder')}
471
+ controls={[
472
+ {
473
+ id: 'status-filter',
474
+ type: 'select',
475
+ value: statusFilter,
476
+ onChange: setStatusFilter,
477
+ placeholder: t('status'),
478
+ options: [
479
+ { value: 'all', label: t('all') },
480
+ { value: 'active', label: t('actives') },
481
+ { value: 'inactive', label: t('inactives') },
482
+ ],
483
+ },
484
+ {
485
+ id: 'parent-filter',
486
+ type: 'select',
487
+ value: parentFilter,
488
+ onChange: setParentFilter,
489
+ placeholder: t('hierarchy'),
490
+ options: [
491
+ { value: 'all', label: t('allHierarchy') },
492
+ { value: 'root', label: t('rootHierarchy') },
493
+ ],
494
+ },
495
+ ]}
496
+ />
473
497
 
474
498
  <div className="space-y-4">
475
499
  {categories.length > 0 ? (
476
- <div className="space-y-4">
500
+ <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
477
501
  {categories.map((category) => (
478
502
  <Card
479
503
  key={category.id}
480
504
  onDoubleClick={() => handleEditCategory(category)}
481
505
  className="cursor-pointer transition-all duration-200 hover:border-primary/20 hover:shadow-md"
482
506
  >
483
- <CardContent className="p-6">
484
- <div className="flex items-start justify-between gap-4">
485
- <div className="flex-1 space-y-3">
486
- <div className="flex items-start space-x-3">
487
- <div
488
- className="mt-1 rounded-full p-2"
489
- style={{
490
- backgroundColor: category.color
491
- ? `${category.color}20`
492
- : '#00000020',
493
- }}
494
- >
495
- <div style={{ color: category.color || '#000000' }}>
496
- {renderIcon(category.icon)}
497
- </div>
507
+ <CardContent className="p-4">
508
+ <div className="flex items-start justify-between gap-3">
509
+ <div className="flex min-w-0 items-start gap-3">
510
+ <div
511
+ className="mt-0.5 rounded-full p-2"
512
+ style={{
513
+ backgroundColor: category.color
514
+ ? `${category.color}20`
515
+ : '#00000020',
516
+ }}
517
+ >
518
+ <div style={{ color: category.color || '#000000' }}>
519
+ {renderIcon(category.icon)}
498
520
  </div>
499
- <div className="flex-1 space-y-2">
500
- <div className="flex items-center gap-2 flex-wrap">
501
- <h3 className="text-lg font-semibold leading-tight">
502
- {category.name}
503
- </h3>
504
- {getStatusBadge(category.status)}
505
- </div>
506
- <div className="flex flex-col gap-1 text-sm text-muted-foreground">
507
- <span>
508
- <strong>{t('slug')}:</strong> {category.slug}
509
- </span>
510
- {category.color && (
511
- <span className="flex items-center gap-2">
512
- <strong>{t('color')}:</strong>
513
- <span
514
- className="inline-block h-4 w-4 rounded border"
515
- style={{
516
- backgroundColor: category.color,
517
- }}
518
- />
519
- {category.color}
520
- </span>
521
- )}
522
- {category.icon && (
523
- <span>
524
- <strong>{t('icon')}:</strong> {category.icon}
521
+ </div>
522
+ <div className="min-w-0 space-y-2">
523
+ <div className="flex items-center gap-2 flex-wrap">
524
+ <h3 className="truncate text-base font-semibold leading-tight">
525
+ {category.name}
526
+ </h3>
527
+ {getStatusBadge(category.status)}
528
+ </div>
529
+ <div className="space-y-1 text-xs text-muted-foreground">
530
+ <p className="truncate">
531
+ <span className="font-medium text-foreground">
532
+ {t('slug')}:
533
+ </span>{' '}
534
+ {category.slug}
535
+ </p>
536
+ {category.color && (
537
+ <p className="flex items-center gap-2">
538
+ <span className="font-medium text-foreground">
539
+ {t('color')}:
525
540
  </span>
526
- )}
527
- </div>
541
+ <span
542
+ className="inline-block h-3.5 w-3.5 rounded border"
543
+ style={{
544
+ backgroundColor: category.color,
545
+ }}
546
+ />
547
+ <span className="truncate">{category.color}</span>
548
+ </p>
549
+ )}
550
+ {category.icon && (
551
+ <p className="truncate">
552
+ <span className="font-medium text-foreground">
553
+ {t('icon')}:
554
+ </span>{' '}
555
+ {category.icon}
556
+ </p>
557
+ )}
528
558
  </div>
529
559
  </div>
530
560
  </div>
561
+ </div>
531
562
 
532
- <div className="flex flex-col gap-2">
533
- <Button
534
- variant="outline"
535
- size="sm"
536
- onClick={() => handleEditCategory(category)}
537
- className="transition-colors hover:border-blue-200 hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-950"
538
- >
539
- <Edit className="mr-1 h-4 w-4" />
540
- {t('edit')}
541
- </Button>
542
-
543
- <AlertDialog>
544
- <AlertDialogTrigger asChild>
545
- <Button
546
- variant="outline"
547
- size="sm"
548
- className="bg-transparent transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950"
563
+ <div className="mt-4 flex items-center justify-end gap-2">
564
+ <Button
565
+ variant="outline"
566
+ size="sm"
567
+ onClick={() => handleEditCategory(category)}
568
+ className="transition-colors hover:border-blue-200 hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-950"
569
+ >
570
+ <Edit className="mr-1 h-4 w-4" />
571
+ {t('edit')}
572
+ </Button>
573
+
574
+ <AlertDialog>
575
+ <AlertDialogTrigger asChild>
576
+ <Button
577
+ variant="outline"
578
+ size="sm"
579
+ className="bg-transparent transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950"
580
+ >
581
+ <Trash2 className="mr-1 h-4 w-4" />
582
+ {t('delete')}
583
+ </Button>
584
+ </AlertDialogTrigger>
585
+ <AlertDialogContent>
586
+ <AlertDialogHeader>
587
+ <AlertDialogTitle>
588
+ {t('confirmDelete')}
589
+ </AlertDialogTitle>
590
+ <AlertDialogDescription>
591
+ {t('deleteDescription')}
592
+ </AlertDialogDescription>
593
+ </AlertDialogHeader>
594
+ <AlertDialogFooter>
595
+ <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
596
+ <AlertDialogAction
597
+ onClick={() =>
598
+ handleDeleteCategory(Number(category.category_id))
599
+ }
600
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
549
601
  >
550
- <Trash2 className="mr-1 h-4 w-4" />
551
602
  {t('delete')}
552
- </Button>
553
- </AlertDialogTrigger>
554
- <AlertDialogContent>
555
- <AlertDialogHeader>
556
- <AlertDialogTitle>
557
- {t('confirmDelete')}
558
- </AlertDialogTitle>
559
- <AlertDialogDescription>
560
- {t('deleteDescription')}
561
- </AlertDialogDescription>
562
- </AlertDialogHeader>
563
- <AlertDialogFooter>
564
- <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
565
- <AlertDialogAction
566
- onClick={() =>
567
- handleDeleteCategory(
568
- Number(category.category_id)
569
- )
570
- }
571
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
572
- >
573
- {t('delete')}
574
- </AlertDialogAction>
575
- </AlertDialogFooter>
576
- </AlertDialogContent>
577
- </AlertDialog>
578
- </div>
603
+ </AlertDialogAction>
604
+ </AlertDialogFooter>
605
+ </AlertDialogContent>
606
+ </AlertDialog>
579
607
  </div>
580
608
  </CardContent>
581
609
  </Card>
582
610
  ))}
583
611
  </div>
584
612
  ) : (
585
- <Card>
586
- <CardContent className="p-12 text-center">
587
- <div className="flex flex-col items-center space-y-4">
588
- <Layers className="h-12 w-12 text-muted-foreground" />
589
- <div>
590
- <h3 className="text-lg font-semibold">
591
- {t('noCategoriesFound')}
592
- </h3>
593
- <p className="text-muted-foreground">{t('adjustFilters')}</p>
594
- </div>
595
- <Button onClick={handleNewCategory}>
596
- <Plus className="mr-2 h-4 w-4" />
597
- {t('createFirstCategory')}
598
- </Button>
599
- </div>
600
- </CardContent>
601
- </Card>
613
+ <EmptyState
614
+ icon={<Layers className="h-12 w-12" />}
615
+ title={t('noCategoriesFound')}
616
+ description={t('adjustFilters')}
617
+ actionLabel={t('createFirstCategory')}
618
+ actionIcon={<Plus className="mr-2 h-4 w-4" />}
619
+ onAction={handleNewCategory}
620
+ />
602
621
  )}
603
622
 
604
623
  <PaginationFooter
@@ -606,42 +625,77 @@ export default function CategoryPage() {
606
625
  pageSize={pageSize}
607
626
  totalItems={total}
608
627
  onPageChange={setPage}
609
- onPageSizeChange={setPageSize}
628
+ onPageSizeChange={(nextPageSize) => {
629
+ setPageSize(nextPageSize);
630
+ setPage(1);
631
+ }}
610
632
  pageSizeOptions={[10, 20, 30, 40, 50]}
611
633
  />
612
634
  </div>
613
635
 
614
- <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
615
- <DialogContent className="max-h-[95vh] max-w-2xl overflow-y-auto">
616
- <DialogHeader>
617
- <DialogTitle className="flex items-center space-x-2">
636
+ <Sheet
637
+ open={isEditDialogOpen}
638
+ onOpenChange={(open) => {
639
+ setIsEditDialogOpen(open);
640
+ if (!open) {
641
+ setIsNewCategory(false);
642
+ setEditingCategoryId(null);
643
+ setLocaleData({});
644
+ form.reset();
645
+ }
646
+ }}
647
+ >
648
+ <SheetContent className="w-full overflow-y-auto sm:max-w-2xl">
649
+ <SheetHeader>
650
+ <SheetTitle className="flex items-center space-x-2">
618
651
  <Edit className="h-5 w-5" />
619
652
  <span>
620
653
  {isNewCategory ? t('newCategoryTitle') : t('editCategory')}
621
654
  </span>
622
- </DialogTitle>
623
- <DialogDescription>
655
+ </SheetTitle>
656
+ <SheetDescription>
624
657
  {isNewCategory ? t('createDescription') : t('editDescription')}
625
- </DialogDescription>
626
- </DialogHeader>
627
-
628
- {selectedCategory && (
629
- <div className="space-y-4">
630
- <div className="space-y-2">
631
- <Label
632
- htmlFor="locale-select"
633
- className="flex items-center gap-2"
634
- >
658
+ </SheetDescription>
659
+ </SheetHeader>
660
+
661
+ <Form {...form}>
662
+ <form onSubmit={handleSaveCategory} className="space-y-4 px-4">
663
+ <FormItem>
664
+ <FormLabel className="flex items-center gap-2">
635
665
  <Globe className="h-4 w-4" />
636
666
  {t('language')}
637
- </Label>
667
+ </FormLabel>
638
668
  <Select
639
669
  value={selectedLocale}
640
- onValueChange={setSelectedLocale}
670
+ onValueChange={(nextLocale) => {
671
+ const currentName = form.getValues('name');
672
+ const currentLocale = selectedLocale;
673
+ const nextLocaleData = {
674
+ ...localeData,
675
+ };
676
+
677
+ if (currentLocale) {
678
+ nextLocaleData[currentLocale] = {
679
+ name: currentName,
680
+ };
681
+ }
682
+
683
+ if (!nextLocaleData[nextLocale]) {
684
+ nextLocaleData[nextLocale] = {
685
+ name: '',
686
+ };
687
+ }
688
+
689
+ setLocaleData(nextLocaleData);
690
+ setSelectedLocale(nextLocale);
691
+ form.setValue('name', nextLocaleData[nextLocale].name);
692
+ }}
641
693
  >
642
- <SelectTrigger id="locale-select">
643
- <SelectValue placeholder={t('selectLanguage')} />
644
- </SelectTrigger>
694
+ <FormControl>
695
+ <SelectTrigger id="locale-select">
696
+ <SelectValue placeholder={t('selectLanguage')} />
697
+ </SelectTrigger>
698
+ </FormControl>
645
699
  <SelectContent>
646
700
  {locales.map((locale: Locale) => (
647
701
  <SelectItem key={locale.code} value={locale.code}>
@@ -650,171 +704,167 @@ export default function CategoryPage() {
650
704
  ))}
651
705
  </SelectContent>
652
706
  </Select>
653
- </div>
654
-
655
- <div className="space-y-2">
656
- <Label htmlFor="name">{t('nameRequired')}</Label>
657
- <Input
658
- id="name"
659
- placeholder={t('namePlaceholder')}
660
- value={selectedCategory.locale?.[selectedLocale]?.name || ''}
661
- onChange={(e) =>
662
- setSelectedCategory({
663
- ...selectedCategory,
664
- locale: {
665
- ...selectedCategory.locale,
666
- [selectedLocale]: {
667
- ...selectedCategory.locale?.[selectedLocale],
668
- name: e.target.value,
669
- },
670
- },
671
- })
672
- }
673
- />
674
- </div>
675
-
676
- <div className="space-y-2">
677
- <Label htmlFor="slug">{t('slugRequired')}</Label>
678
- <Input
679
- id="slug"
680
- placeholder={t('slugPlaceholder')}
681
- value={selectedCategory.slug || ''}
682
- onChange={(e) =>
683
- setSelectedCategory({
684
- ...selectedCategory,
685
- slug: e.target.value,
686
- })
687
- }
688
- />
689
- </div>
690
-
691
- <div className="space-y-2">
692
- <Label htmlFor="color">{t('color')}</Label>
693
- <div className="flex gap-2">
694
- <Input
695
- id="color"
696
- type="color"
697
- className="h-10 w-20"
698
- value={selectedCategory.color || '#000000'}
699
- onChange={(e) =>
700
- setSelectedCategory({
701
- ...selectedCategory,
702
- color: e.target.value,
703
- })
704
- }
705
- />
706
- <Input
707
- placeholder={t('colorPlaceholder')}
708
- value={selectedCategory.color || '#000000'}
709
- onChange={(e) =>
710
- setSelectedCategory({
711
- ...selectedCategory,
712
- color: e.target.value,
713
- })
714
- }
715
- />
716
- </div>
717
- </div>
718
-
719
- <div className="space-y-2">
720
- <Label htmlFor="icon">{t('icon')}</Label>
721
- <div className="flex gap-2 items-center">
722
- <div className="shrink-0">
723
- {renderIcon(selectedCategory.icon)}
724
- </div>
725
- <Input
726
- id="icon"
727
- placeholder={t('iconPlaceholder')}
728
- value={selectedCategory.icon || ''}
729
- onChange={(e) =>
730
- setSelectedCategory({
731
- ...selectedCategory,
732
- icon: e.target.value,
733
- })
734
- }
735
- />
736
- </div>
737
- </div>
738
-
739
- <div className="space-y-2">
740
- <Label htmlFor="parent">{t('parentCategory')}</Label>
741
- <Select
742
- value={selectedCategory.category_id?.toString() || 'none'}
743
- onValueChange={(value) =>
744
- setSelectedCategory({
745
- ...selectedCategory,
746
- category_id: value === 'none' ? null : Number(value),
747
- })
748
- }
749
- >
750
- <SelectTrigger className="w-full" id="parent">
751
- <SelectValue placeholder={t('selectParent')} />
752
- </SelectTrigger>
753
- <SelectContent>
754
- <SelectItem value="none">{t('noneRoot')}</SelectItem>
755
- {Array.isArray(rootCategories) &&
756
- rootCategories.map((cat: Category) => (
757
- <SelectItem
758
- key={cat.id}
759
- value={String(cat.category_id)}
760
- >
761
- {cat.name}
707
+ </FormItem>
708
+
709
+ <FormField
710
+ control={form.control}
711
+ name="name"
712
+ render={({ field }) => (
713
+ <FormItem>
714
+ <FormLabel>{t('nameRequired')}</FormLabel>
715
+ <FormControl>
716
+ <Input
717
+ {...field}
718
+ placeholder={t('namePlaceholder')}
719
+ onChange={(event) => {
720
+ field.onChange(event.target.value);
721
+ if (selectedLocale) {
722
+ setLocaleData((current) => ({
723
+ ...current,
724
+ [selectedLocale]: {
725
+ name: event.target.value,
726
+ },
727
+ }));
728
+ }
729
+ }}
730
+ />
731
+ </FormControl>
732
+ <FormMessage />
733
+ </FormItem>
734
+ )}
735
+ />
736
+
737
+ <FormField
738
+ control={form.control}
739
+ name="slug"
740
+ render={({ field }) => (
741
+ <FormItem>
742
+ <FormLabel>{t('slugRequired')}</FormLabel>
743
+ <FormControl>
744
+ <Input {...field} placeholder={t('slugPlaceholder')} />
745
+ </FormControl>
746
+ <FormMessage />
747
+ </FormItem>
748
+ )}
749
+ />
750
+
751
+ <FormField
752
+ control={form.control}
753
+ name="color"
754
+ render={({ field }) => (
755
+ <FormItem>
756
+ <FormLabel>{t('color')}</FormLabel>
757
+ <FormControl>
758
+ <div className="flex gap-2">
759
+ <Input
760
+ type="color"
761
+ className="h-10 w-20"
762
+ value={field.value || '#000000'}
763
+ onChange={(event) =>
764
+ field.onChange(event.target.value)
765
+ }
766
+ />
767
+ <Input
768
+ value={field.value || '#000000'}
769
+ onChange={(event) =>
770
+ field.onChange(event.target.value)
771
+ }
772
+ placeholder={t('colorPlaceholder')}
773
+ />
774
+ </div>
775
+ </FormControl>
776
+ <FormMessage />
777
+ </FormItem>
778
+ )}
779
+ />
780
+
781
+ <FormField
782
+ control={form.control}
783
+ name="icon"
784
+ render={({ field }) => (
785
+ <FormItem>
786
+ <FormLabel>{t('icon')}</FormLabel>
787
+ <FormControl>
788
+ <div className="flex items-center gap-2">
789
+ <div className="shrink-0">
790
+ {renderIcon(field.value)}
791
+ </div>
792
+ <Input {...field} placeholder={t('iconPlaceholder')} />
793
+ </div>
794
+ </FormControl>
795
+ <FormMessage />
796
+ </FormItem>
797
+ )}
798
+ />
799
+
800
+ <FormField
801
+ control={form.control}
802
+ name="category_id"
803
+ render={({ field }) => (
804
+ <FormItem>
805
+ <FormLabel>{t('parentCategory')}</FormLabel>
806
+ <Select value={field.value} onValueChange={field.onChange}>
807
+ <FormControl>
808
+ <SelectTrigger className="w-full" id="parent">
809
+ <SelectValue placeholder={t('selectParent')} />
810
+ </SelectTrigger>
811
+ </FormControl>
812
+ <SelectContent>
813
+ <SelectItem value="none">{t('noneRoot')}</SelectItem>
814
+ {Array.isArray(rootCategories) &&
815
+ rootCategories.map((cat: Category) => (
816
+ <SelectItem
817
+ key={cat.id}
818
+ value={String(cat.category_id)}
819
+ >
820
+ {cat.name}
821
+ </SelectItem>
822
+ ))}
823
+ </SelectContent>
824
+ </Select>
825
+ <FormMessage />
826
+ </FormItem>
827
+ )}
828
+ />
829
+
830
+ <FormField
831
+ control={form.control}
832
+ name="status"
833
+ render={({ field }) => (
834
+ <FormItem>
835
+ <FormLabel>{t('status')}</FormLabel>
836
+ <Select value={field.value} onValueChange={field.onChange}>
837
+ <FormControl>
838
+ <SelectTrigger className="w-full" id="status">
839
+ <SelectValue placeholder={t('selectStatus')} />
840
+ </SelectTrigger>
841
+ </FormControl>
842
+ <SelectContent>
843
+ <SelectItem value="active">{t('active')}</SelectItem>
844
+ <SelectItem value="inactive">
845
+ {t('inactive')}
762
846
  </SelectItem>
763
- ))}
764
- </SelectContent>
765
- </Select>
766
- </div>
767
-
768
- <div className="space-y-2">
769
- <Label htmlFor="status">{t('status')}</Label>
770
- <Select
771
- value={selectedCategory.status}
772
- onValueChange={(value) =>
773
- setSelectedCategory({
774
- ...selectedCategory,
775
- status: value,
776
- })
777
- }
847
+ </SelectContent>
848
+ </Select>
849
+ <FormMessage />
850
+ </FormItem>
851
+ )}
852
+ />
853
+
854
+ <SheetFooter className="px-0">
855
+ <Button
856
+ type="submit"
857
+ disabled={!form.formState.isValid}
858
+ className="w-full transition-colors hover:bg-primary/90"
778
859
  >
779
- <SelectTrigger className="w-full" id="status">
780
- <SelectValue placeholder={t('selectStatus')} />
781
- </SelectTrigger>
782
- <SelectContent>
783
- <SelectItem value="active">{t('active')}</SelectItem>
784
- <SelectItem value="inactive">{t('inactive')}</SelectItem>
785
- </SelectContent>
786
- </Select>
787
- </div>
788
- </div>
789
- )}
790
-
791
- <DialogFooter className="mt-4">
792
- <Button
793
- variant="outline"
794
- onClick={() => {
795
- setIsEditDialogOpen(false);
796
- setSelectedCategory(null);
797
- setIsNewCategory(false);
798
- }}
799
- >
800
- <X className="mr-2 h-4 w-4" />
801
- {t('cancel')}
802
- </Button>
803
- <Button
804
- onClick={handleSaveCategory}
805
- disabled={
806
- !selectedCategory?.slug ||
807
- !selectedCategory?.locale ||
808
- !Object.values(selectedCategory.locale).some((l: any) => l.name)
809
- }
810
- className="transition-colors hover:bg-primary/90"
811
- >
812
- <Save className="mr-2 h-4 w-4" />
813
- {isNewCategory ? t('createCategoryButton') : t('saveChanges')}
814
- </Button>
815
- </DialogFooter>
816
- </DialogContent>
817
- </Dialog>
818
- </div>
860
+ <Save className="mr-2 h-4 w-4" />
861
+ {isNewCategory ? t('createCategoryButton') : t('saveChanges')}
862
+ </Button>
863
+ </SheetFooter>
864
+ </form>
865
+ </Form>
866
+ </SheetContent>
867
+ </Sheet>
868
+ </Page>
819
869
  );
820
870
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/category",
3
- "version": "0.0.279",
3
+ "version": "0.0.285",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -10,10 +10,10 @@
10
10
  "@nestjs/jwt": "^11",
11
11
  "@nestjs/mapped-types": "*",
12
12
  "@hed-hog/api-prisma": "0.0.5",
13
- "@hed-hog/core": "0.0.279",
14
- "@hed-hog/api-pagination": "0.0.6",
15
13
  "@hed-hog/api": "0.0.4",
16
- "@hed-hog/api-locale": "0.0.13"
14
+ "@hed-hog/api-locale": "0.0.13",
15
+ "@hed-hog/core": "0.0.285",
16
+ "@hed-hog/api-pagination": "0.0.6"
17
17
  },
18
18
  "exports": {
19
19
  ".": {