@hed-hog/tag 0.0.278 → 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.
@@ -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,12 +38,23 @@ 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';
41
- import { Edit, Save, Tag as TagIcon, Trash2, X } from 'lucide-react';
51
+ import { zodResolver } from '@hookform/resolvers/zod';
52
+ import { Edit, Save, Tag as TagIcon, Trash2 } from 'lucide-react';
42
53
  import { useTranslations } from 'next-intl';
43
- import { useEffect, useState } from 'react';
54
+ import { useEffect, useMemo, useState } from 'react';
55
+ import { useForm } from 'react-hook-form';
44
56
  import { toast } from 'sonner';
57
+ import { z } from 'zod';
45
58
 
46
59
  type PaginationResult<T> = {
47
60
  data: T[];
@@ -57,22 +70,44 @@ type Tag = {
57
70
  status: 'active' | 'inactive';
58
71
  };
59
72
 
73
+ const PAGE_SIZE_OPTIONS: number[] = [10, 20, 30, 40, 50];
74
+
60
75
  export default function TagPage() {
61
76
  const t = useTranslations('tag.Tag');
62
- const [tags, setTags] = useState<Tag[]>([]);
63
- const [selectedTag, setSelectedTag] = useState<Partial<Tag> | null>(null);
64
- const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
65
- const [isNewTag, setIsNewTag] = useState(false);
77
+ const formSchema = useMemo(
78
+ () =>
79
+ z.object({
80
+ slug: z.string().trim().min(2, t('validation.slugMin')),
81
+ color: z
82
+ .string()
83
+ .regex(/^#[0-9A-Fa-f]{6}$/, t('validation.colorInvalid')),
84
+ status: z.enum(['active', 'inactive']),
85
+ }),
86
+ [t]
87
+ );
88
+
89
+ type FormValues = z.infer<typeof formSchema>;
90
+
91
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
92
+ const [editingTagId, setEditingTagId] = useState<number | null>(null);
66
93
  const [searchTerm, setSearchTerm] = useState('');
67
94
  const debouncedSearch = useDebounce(searchTerm);
68
95
  const [page, setPage] = useState(1);
69
- const [pageSize, setPageSize] = useState(10);
96
+ const [pageSize, setPageSize] = useState<number>(10);
70
97
  const { request } = useApp();
71
98
 
72
- const {
73
- data: { data, total },
74
- refetch: refetchTag,
75
- } = useQuery<PaginationResult<Tag>>({
99
+ const form = useForm<FormValues>({
100
+ resolver: zodResolver(formSchema),
101
+ defaultValues: {
102
+ slug: '',
103
+ color: '#000000',
104
+ status: 'active',
105
+ },
106
+ });
107
+
108
+ const { data: tagResult, refetch: refetchTag } = useQuery<
109
+ PaginationResult<Tag>
110
+ >({
76
111
  queryKey: ['tag', debouncedSearch, page, pageSize],
77
112
  queryFn: async () => {
78
113
  const response = await request({
@@ -85,13 +120,8 @@ export default function TagPage() {
85
120
  });
86
121
  return response.data as PaginationResult<Tag>;
87
122
  },
88
- initialData: {
89
- data: [],
90
- total: 0,
91
- page: 1,
92
- pageSize: 10,
93
- },
94
123
  });
124
+ const { data = [], total = 0 } = tagResult ?? {};
95
125
 
96
126
  const { data: statsData, refetch: refetchStats } = useQuery<any>({
97
127
  queryKey: ['tag-stats'],
@@ -103,51 +133,47 @@ export default function TagPage() {
103
133
  },
104
134
  });
105
135
 
106
- useEffect(() => {
107
- if (data) {
108
- setTags(data);
109
- }
110
- }, [data]);
111
-
112
136
  const handleNewTag = (): void => {
113
- setSelectedTag({
137
+ form.reset({
114
138
  slug: '',
115
139
  color: '#000000',
116
140
  status: 'active',
117
141
  });
118
- setIsNewTag(true);
119
- setIsEditDialogOpen(true);
142
+ setEditingTagId(null);
143
+ setIsSheetOpen(true);
120
144
  };
121
145
 
122
146
  const handleEditTag = async (tag: Tag): Promise<void> => {
123
147
  try {
124
- const response = await request({
148
+ const response = await request<Tag>({
125
149
  url: `/tag/${tag.id}`,
126
150
  method: 'GET',
127
151
  });
128
152
 
129
- setSelectedTag(response.data);
130
- setIsNewTag(false);
131
- setIsEditDialogOpen(true);
153
+ form.reset({
154
+ slug: response.data.slug ?? '',
155
+ color: response.data.color ?? '#000000',
156
+ status: response.data.status ?? 'active',
157
+ });
158
+ setEditingTagId(tag.id);
159
+ setIsSheetOpen(true);
132
160
  } catch (error) {
133
161
  console.error(error);
134
162
  toast.error(t('errorLoading'));
135
163
  }
136
164
  };
137
165
 
138
- const handleSaveTag = async () => {
139
- if (!selectedTag) return;
140
-
166
+ const handleSaveTag = async (values: FormValues) => {
141
167
  const payload = {
142
- slug: selectedTag.slug,
143
- color: selectedTag.color,
144
- status: selectedTag.status,
168
+ slug: values.slug,
169
+ color: values.color,
170
+ status: values.status,
145
171
  };
146
172
 
147
173
  try {
148
- if (selectedTag.id) {
174
+ if (editingTagId) {
149
175
  await request({
150
- url: `/tag/${selectedTag.id}`,
176
+ url: `/tag/${editingTagId}`,
151
177
  method: 'PATCH',
152
178
  data: payload,
153
179
  });
@@ -161,7 +187,7 @@ export default function TagPage() {
161
187
  toast.success(t('successCreate'));
162
188
  }
163
189
 
164
- setIsEditDialogOpen(false);
190
+ setIsSheetOpen(false);
165
191
  await refetchTag();
166
192
  await refetchStats();
167
193
  } catch (error) {
@@ -187,20 +213,19 @@ export default function TagPage() {
187
213
 
188
214
  const handleSearchChange = (value: string): void => {
189
215
  setSearchTerm(value);
216
+ setPage(1);
190
217
  };
191
218
 
192
219
  useEffect(() => {
193
- refetchTag();
194
220
  refetchStats();
195
- }, [isEditDialogOpen, debouncedSearch, page, pageSize]);
221
+ }, [isSheetOpen, refetchStats]);
222
+
223
+ const isNewTag = editingTagId === null;
196
224
 
197
225
  return (
198
- <div className="flex flex-col h-screen px-4">
226
+ <Page>
199
227
  <PageHeader
200
- breadcrumbs={[
201
- { label: 'Home', href: '/' },
202
- { label: t('description') },
203
- ]}
228
+ breadcrumbs={[{ label: 'Home', href: '/' }, { label: t('title') }]}
204
229
  actions={[
205
230
  {
206
231
  label: t('newTag'),
@@ -212,142 +237,128 @@ export default function TagPage() {
212
237
  description={t('description')}
213
238
  />
214
239
 
215
- <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
216
- <Card className="transition-shadow hover:shadow-md p-0">
217
- <CardContent className="p-4">
218
- <div className="flex items-center space-x-3">
219
- <div className="rounded-full bg-blue-100 p-2 dark:bg-blue-900">
220
- <TagIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
221
- </div>
222
- <div>
223
- <p className="text-sm font-medium text-muted-foreground">
224
- {t('totalTags')}
225
- </p>
226
- <p className="text-2xl font-bold">{statsData?.total || 0}</p>
227
- </div>
228
- </div>
229
- </CardContent>
230
- </Card>
231
-
232
- <Card className="transition-shadow hover:shadow-md p-0">
233
- <CardContent className="p-4">
234
- <div className="flex items-center space-x-3">
235
- <div className="rounded-full bg-green-100 p-2 dark:bg-green-900">
236
- <TagIcon className="h-6 w-6 text-green-600 dark:text-green-400" />
237
- </div>
238
- <div>
239
- <p className="text-sm font-medium text-muted-foreground">
240
- {t('activeTags')}
241
- </p>
242
- <p className="text-2xl font-bold">{statsData?.active || 0}</p>
243
- </div>
244
- </div>
245
- </CardContent>
246
- </Card>
247
-
248
- <Card className="transition-shadow hover:shadow-md p-0">
249
- <CardContent className="p-4">
250
- <div className="flex items-center space-x-3">
251
- <div className="rounded-full bg-gray-100 p-2 dark:bg-gray-900">
252
- <TagIcon className="h-6 w-6 text-gray-600 dark:text-gray-400" />
253
- </div>
254
- <div>
255
- <p className="text-sm font-medium text-muted-foreground">
256
- {t('inactiveTags')}
257
- </p>
258
- <p className="text-2xl font-bold">{statsData?.inactive || 0}</p>
259
- </div>
260
- </div>
261
- </CardContent>
262
- </Card>
263
- </div>
240
+ <StatsCards
241
+ stats={[
242
+ {
243
+ title: t('totalTags'),
244
+ value: statsData?.total || 0,
245
+ icon: <TagIcon className="h-5 w-5" />,
246
+ iconBgColor: 'bg-blue-100 dark:bg-blue-900',
247
+ iconColor: 'text-blue-600 dark:text-blue-400',
248
+ },
249
+ {
250
+ title: t('activeTags'),
251
+ value: statsData?.active || 0,
252
+ icon: <TagIcon className="h-5 w-5" />,
253
+ iconBgColor: 'bg-green-100 dark:bg-green-900',
254
+ iconColor: 'text-green-600 dark:text-green-400',
255
+ },
256
+ {
257
+ title: t('inactiveTags'),
258
+ value: statsData?.inactive || 0,
259
+ icon: <TagIcon className="h-5 w-5" />,
260
+ iconBgColor: 'bg-gray-100 dark:bg-gray-900',
261
+ iconColor: 'text-gray-600 dark:text-gray-400',
262
+ },
263
+ ]}
264
+ className="grid-cols-1 md:grid-cols-3"
265
+ />
264
266
 
265
- <div className="mb-4 flex flex-col gap-4 md:flex-row mt-4">
266
- <SearchBar
267
- searchQuery={searchTerm}
268
- onSearchChange={handleSearchChange}
269
- onSearch={() => refetchTag()}
270
- placeholder={t('searchPlaceholder')}
271
- />
272
- </div>
267
+ <SearchBar
268
+ searchQuery={searchTerm}
269
+ onSearchChange={handleSearchChange}
270
+ onSearch={() => refetchTag()}
271
+ placeholder={t('searchPlaceholder')}
272
+ />
273
273
 
274
- <div className="space-y-3">
275
- {tags.length === 0 ? (
276
- <Card>
277
- <CardContent className="flex flex-col items-center justify-center p-12">
278
- <TagIcon className="mb-4 h-12 w-12 text-muted-foreground" />
279
- <p className="text-lg font-medium text-muted-foreground">
280
- {t('noTagsFound')}
281
- </p>
282
- <p className="text-sm text-muted-foreground">
283
- {searchTerm ? t('adjustSearch') : t('createNewTag')}
284
- </p>
285
- </CardContent>
286
- </Card>
274
+ <div>
275
+ {data.length === 0 ? (
276
+ <EmptyState
277
+ icon={<TagIcon className="h-6 w-6" />}
278
+ title={t('noTagsFound')}
279
+ description={searchTerm ? t('adjustSearch') : t('createNewTag')}
280
+ actionLabel={searchTerm ? t('clearSearch') : t('newTag')}
281
+ onAction={() => {
282
+ if (searchTerm) {
283
+ setSearchTerm('');
284
+ setPage(1);
285
+ return;
286
+ }
287
+ handleNewTag();
288
+ }}
289
+ />
287
290
  ) : (
288
- tags.map((tag) => (
289
- <Card
290
- key={tag.id}
291
- onDoubleClick={() => handleEditTag(tag)}
292
- className="cursor-pointer transition-shadow hover:shadow-md"
293
- >
294
- <CardContent className="flex items-center justify-between p-6">
295
- <div className="flex items-center space-x-4">
296
- <div
297
- className="h-10 w-10 rounded-md"
298
- style={{ backgroundColor: tag.color }}
299
- />
300
- <div>
301
- <div className="flex items-center gap-2">
302
- <h2 className="font-semibold text-lg">#{tag.slug}</h2>
303
- <Badge
304
- variant={
305
- tag.status === 'active' ? 'default' : 'secondary'
306
- }
307
- >
308
- {tag.status === 'active' ? t('active') : t('inactive')}
309
- </Badge>
291
+ <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
292
+ {data.map((tag) => (
293
+ <Card
294
+ key={tag.id}
295
+ onDoubleClick={() => handleEditTag(tag)}
296
+ className="cursor-pointer transition-shadow hover:shadow-md"
297
+ >
298
+ <CardContent className="p-4">
299
+ <div className="flex items-start justify-between gap-2">
300
+ <div className="flex items-center gap-3">
301
+ <div
302
+ className="h-9 w-9 rounded-md border"
303
+ style={{ backgroundColor: tag.color }}
304
+ />
305
+ <div className="min-w-0">
306
+ <h2 className="truncate text-base font-semibold">
307
+ #{tag.slug}
308
+ </h2>
309
+ <p className="text-xs text-muted-foreground">
310
+ {tag.color}
311
+ </p>
312
+ </div>
310
313
  </div>
314
+ <Badge
315
+ variant={
316
+ tag.status === 'active' ? 'default' : 'secondary'
317
+ }
318
+ >
319
+ {tag.status === 'active' ? t('active') : t('inactive')}
320
+ </Badge>
321
+ </div>
322
+
323
+ <div className="mt-3 flex items-center justify-end gap-1">
324
+ <Button
325
+ variant="ghost"
326
+ size="icon"
327
+ onClick={() => handleEditTag(tag)}
328
+ >
329
+ <Edit className="h-4 w-4" />
330
+ </Button>
331
+ <AlertDialog>
332
+ <AlertDialogTrigger asChild>
333
+ <Button variant="ghost" size="icon">
334
+ <Trash2 className="h-4 w-4 text-destructive" />
335
+ </Button>
336
+ </AlertDialogTrigger>
337
+ <AlertDialogContent>
338
+ <AlertDialogHeader>
339
+ <AlertDialogTitle>
340
+ {t('confirmDelete')}
341
+ </AlertDialogTitle>
342
+ <AlertDialogDescription>
343
+ {t('deleteDescription')}
344
+ </AlertDialogDescription>
345
+ </AlertDialogHeader>
346
+ <AlertDialogFooter>
347
+ <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
348
+ <AlertDialogAction
349
+ onClick={() => handleDeleteTag(tag.id)}
350
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
351
+ >
352
+ {t('delete')}
353
+ </AlertDialogAction>
354
+ </AlertDialogFooter>
355
+ </AlertDialogContent>
356
+ </AlertDialog>
311
357
  </div>
312
- </div>
313
- <div className="flex space-x-2">
314
- <Button
315
- variant="ghost"
316
- size="icon"
317
- onClick={() => handleEditTag(tag)}
318
- >
319
- <Edit className="h-4 w-4" />
320
- </Button>
321
- <AlertDialog>
322
- <AlertDialogTrigger asChild>
323
- <Button variant="ghost" size="icon">
324
- <Trash2 className="h-4 w-4 text-destructive" />
325
- </Button>
326
- </AlertDialogTrigger>
327
- <AlertDialogContent>
328
- <AlertDialogHeader>
329
- <AlertDialogTitle>
330
- {t('confirmDelete')}
331
- </AlertDialogTitle>
332
- <AlertDialogDescription>
333
- {t('deleteDescription')}
334
- </AlertDialogDescription>
335
- </AlertDialogHeader>
336
- <AlertDialogFooter>
337
- <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
338
- <AlertDialogAction
339
- onClick={() => handleDeleteTag(tag.id)}
340
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
341
- >
342
- {t('delete')}
343
- </AlertDialogAction>
344
- </AlertDialogFooter>
345
- </AlertDialogContent>
346
- </AlertDialog>
347
- </div>
348
- </CardContent>
349
- </Card>
350
- ))
358
+ </CardContent>
359
+ </Card>
360
+ ))}
361
+ </div>
351
362
  )}
352
363
  </div>
353
364
 
@@ -357,89 +368,102 @@ export default function TagPage() {
357
368
  totalItems={total}
358
369
  onPageChange={setPage}
359
370
  onPageSizeChange={setPageSize}
360
- pageSizeOptions={[10, 20, 30, 40, 50]}
371
+ pageSizeOptions={[...PAGE_SIZE_OPTIONS]}
361
372
  />
362
373
 
363
- <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
364
- <DialogContent className="max-h-[90vh] max-w-2xl overflow-y-auto">
365
- <DialogHeader>
366
- <DialogTitle>
367
- {isNewTag ? t('createTag') : t('editTag')}
368
- </DialogTitle>
369
- <DialogDescription>
374
+ <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
375
+ <SheetContent className="w-full overflow-y-auto sm:max-w-lg">
376
+ <SheetHeader>
377
+ <SheetTitle>{isNewTag ? t('createTag') : t('editTag')}</SheetTitle>
378
+ <SheetDescription>
370
379
  {isNewTag ? t('createDescription') : t('editDescription')}
371
- </DialogDescription>
372
- </DialogHeader>
380
+ </SheetDescription>
381
+ </SheetHeader>
373
382
 
374
- <div className="space-y-4">
375
- <div className="space-y-2">
376
- <Label htmlFor="slug">{t('slug')}</Label>
377
- <Input
378
- id="slug"
379
- value={selectedTag?.slug || ''}
380
- onChange={(e) =>
381
- setSelectedTag({ ...selectedTag, slug: e.target.value })
382
- }
383
- placeholder={t('slugPlaceholder')}
383
+ <Form {...form}>
384
+ <form
385
+ onSubmit={form.handleSubmit(handleSaveTag)}
386
+ className="mt-6 space-y-4 px-4"
387
+ >
388
+ <FormField
389
+ control={form.control}
390
+ name="slug"
391
+ render={({ field }) => (
392
+ <FormItem>
393
+ <FormLabel>{t('slug')}</FormLabel>
394
+ <FormControl>
395
+ <Input {...field} placeholder={t('slugPlaceholder')} />
396
+ </FormControl>
397
+ <FormMessage />
398
+ </FormItem>
399
+ )}
384
400
  />
385
- </div>
386
401
 
387
- <div className="space-y-2">
388
- <Label htmlFor="color">{t('color')}</Label>
389
- <div className="flex gap-2">
390
- <Input
391
- id="color"
392
- type="color"
393
- value={selectedTag?.color || '#000000'}
394
- onChange={(e) =>
395
- setSelectedTag({ ...selectedTag, color: e.target.value })
396
- }
397
- className="h-10 w-20"
398
- />
399
- <Input
400
- value={selectedTag?.color || '#000000'}
401
- onChange={(e) =>
402
- setSelectedTag({ ...selectedTag, color: e.target.value })
403
- }
404
- placeholder={t('colorPlaceholder')}
405
- />
406
- </div>
407
- </div>
402
+ <FormField
403
+ control={form.control}
404
+ name="color"
405
+ render={({ field }) => (
406
+ <FormItem>
407
+ <FormLabel>{t('color')}</FormLabel>
408
+ <FormControl>
409
+ <div className="flex gap-2">
410
+ <Input
411
+ type="color"
412
+ value={field.value}
413
+ onChange={(e) => field.onChange(e.target.value)}
414
+ className="h-10 w-20"
415
+ />
416
+ <Input
417
+ value={field.value}
418
+ onChange={(e) => field.onChange(e.target.value)}
419
+ placeholder={t('colorPlaceholder')}
420
+ />
421
+ </div>
422
+ </FormControl>
423
+ <FormMessage />
424
+ </FormItem>
425
+ )}
426
+ />
408
427
 
409
- <div className="space-y-2">
410
- <Label htmlFor="status">{t('status')}</Label>
411
- <Select
412
- value={selectedTag?.status || 'active'}
413
- onValueChange={(value: 'active' | 'inactive') =>
414
- setSelectedTag({ ...selectedTag, status: value })
415
- }
416
- >
417
- <SelectTrigger className="w-full" id="status">
418
- <SelectValue />
419
- </SelectTrigger>
420
- <SelectContent>
421
- <SelectItem value="active">{t('active')}</SelectItem>
422
- <SelectItem value="inactive">{t('inactive')}</SelectItem>
423
- </SelectContent>
424
- </Select>
425
- </div>
426
- </div>
428
+ <FormField
429
+ control={form.control}
430
+ name="status"
431
+ render={({ field }) => (
432
+ <FormItem>
433
+ <FormLabel>{t('status')}</FormLabel>
434
+ <FormControl>
435
+ <Select
436
+ value={field.value}
437
+ onValueChange={(value: 'active' | 'inactive') =>
438
+ field.onChange(value)
439
+ }
440
+ >
441
+ <SelectTrigger className="w-full">
442
+ <SelectValue />
443
+ </SelectTrigger>
444
+ <SelectContent>
445
+ <SelectItem value="active">{t('active')}</SelectItem>
446
+ <SelectItem value="inactive">
447
+ {t('inactive')}
448
+ </SelectItem>
449
+ </SelectContent>
450
+ </Select>
451
+ </FormControl>
452
+ <FormMessage />
453
+ </FormItem>
454
+ )}
455
+ />
427
456
 
428
- <DialogFooter>
429
- <Button
430
- variant="outline"
431
- onClick={() => setIsEditDialogOpen(false)}
432
- >
433
- <X className="mr-2 h-4 w-4" />
434
- {t('cancel')}
435
- </Button>
436
- <Button onClick={handleSaveTag}>
437
- <Save className="mr-2 h-4 w-4" />
438
- {t('save')}
439
- </Button>
440
- </DialogFooter>
441
- </DialogContent>
442
- </Dialog>
443
- </div>
457
+ <SheetFooter className="px-0">
458
+ <Button type="submit" className="w-full">
459
+ <Save className="mr-2 h-4 w-4" />
460
+ {t('save')}
461
+ </Button>
462
+ </SheetFooter>
463
+ </form>
464
+ </Form>
465
+ </SheetContent>
466
+ </Sheet>
467
+ </Page>
444
468
  );
445
469
  }
@@ -7,6 +7,7 @@
7
7
  "activeTags": "Active Tags",
8
8
  "inactiveTags": "Inactive Tags",
9
9
  "searchPlaceholder": "Search tags...",
10
+ "clearSearch": "Clear search",
10
11
  "noTagsFound": "No tags found",
11
12
  "adjustSearch": "Try adjusting your search",
12
13
  "createNewTag": "Start by creating a new tag",
@@ -31,6 +32,10 @@
31
32
  "successCreate": "Tag created successfully!",
32
33
  "errorSave": "Error saving tag.",
33
34
  "successDelete": "Tag deleted successfully!",
34
- "errorDelete": "Error deleting tag."
35
+ "errorDelete": "Error deleting tag.",
36
+ "validation": {
37
+ "slugMin": "Slug must have at least 2 characters.",
38
+ "colorInvalid": "Enter a valid hex color (e.g. #000000)."
39
+ }
35
40
  }
36
41
  }
@@ -7,6 +7,7 @@
7
7
  "activeTags": "Tags Ativas",
8
8
  "inactiveTags": "Tags Inativas",
9
9
  "searchPlaceholder": "Buscar tags...",
10
+ "clearSearch": "Limpar busca",
10
11
  "noTagsFound": "Nenhuma tag encontrada",
11
12
  "adjustSearch": "Tente ajustar sua busca",
12
13
  "createNewTag": "Comece criando uma nova tag",
@@ -31,6 +32,10 @@
31
32
  "successCreate": "Tag criada com sucesso!",
32
33
  "errorSave": "Erro ao salvar a tag.",
33
34
  "successDelete": "Tag excluída com sucesso!",
34
- "errorDelete": "Erro ao excluir a tag."
35
+ "errorDelete": "Erro ao excluir a tag.",
36
+ "validation": {
37
+ "slugMin": "O slug deve ter pelo menos 2 caracteres.",
38
+ "colorInvalid": "Informe uma cor hexadecimal válida (ex: #000000)."
39
+ }
35
40
  }
36
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/tag",
3
- "version": "0.0.278",
3
+ "version": "0.0.285",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -11,9 +11,9 @@
11
11
  "@nestjs/mapped-types": "*",
12
12
  "@hed-hog/api-prisma": "0.0.5",
13
13
  "@hed-hog/api-pagination": "0.0.6",
14
- "@hed-hog/core": "0.0.278",
15
14
  "@hed-hog/api": "0.0.4",
16
- "@hed-hog/api-locale": "0.0.13"
15
+ "@hed-hog/api-locale": "0.0.13",
16
+ "@hed-hog/core": "0.0.285"
17
17
  },
18
18
  "exports": {
19
19
  ".": {