@hed-hog/tag 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,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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from '@/components/ui/
|
|
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 {
|
|
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,21 +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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
+
>({
|
|
75
111
|
queryKey: ['tag', debouncedSearch, page, pageSize],
|
|
76
112
|
queryFn: async () => {
|
|
77
113
|
const response = await request({
|
|
@@ -84,8 +120,8 @@ export default function TagPage() {
|
|
|
84
120
|
});
|
|
85
121
|
return response.data as PaginationResult<Tag>;
|
|
86
122
|
},
|
|
87
|
-
});
|
|
88
|
-
const { data = [], total = 0 } = tagResult ?? {};
|
|
123
|
+
});
|
|
124
|
+
const { data = [], total = 0 } = tagResult ?? {};
|
|
89
125
|
|
|
90
126
|
const { data: statsData, refetch: refetchStats } = useQuery<any>({
|
|
91
127
|
queryKey: ['tag-stats'],
|
|
@@ -97,51 +133,47 @@ export default function TagPage() {
|
|
|
97
133
|
},
|
|
98
134
|
});
|
|
99
135
|
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
if (data) {
|
|
102
|
-
setTags(data);
|
|
103
|
-
}
|
|
104
|
-
}, [data]);
|
|
105
|
-
|
|
106
136
|
const handleNewTag = (): void => {
|
|
107
|
-
|
|
137
|
+
form.reset({
|
|
108
138
|
slug: '',
|
|
109
139
|
color: '#000000',
|
|
110
140
|
status: 'active',
|
|
111
141
|
});
|
|
112
|
-
|
|
113
|
-
|
|
142
|
+
setEditingTagId(null);
|
|
143
|
+
setIsSheetOpen(true);
|
|
114
144
|
};
|
|
115
145
|
|
|
116
146
|
const handleEditTag = async (tag: Tag): Promise<void> => {
|
|
117
147
|
try {
|
|
118
|
-
const response = await request({
|
|
148
|
+
const response = await request<Tag>({
|
|
119
149
|
url: `/tag/${tag.id}`,
|
|
120
150
|
method: 'GET',
|
|
121
151
|
});
|
|
122
152
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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);
|
|
126
160
|
} catch (error) {
|
|
127
161
|
console.error(error);
|
|
128
162
|
toast.error(t('errorLoading'));
|
|
129
163
|
}
|
|
130
164
|
};
|
|
131
165
|
|
|
132
|
-
const handleSaveTag = async () => {
|
|
133
|
-
if (!selectedTag) return;
|
|
134
|
-
|
|
166
|
+
const handleSaveTag = async (values: FormValues) => {
|
|
135
167
|
const payload = {
|
|
136
|
-
slug:
|
|
137
|
-
color:
|
|
138
|
-
status:
|
|
168
|
+
slug: values.slug,
|
|
169
|
+
color: values.color,
|
|
170
|
+
status: values.status,
|
|
139
171
|
};
|
|
140
172
|
|
|
141
173
|
try {
|
|
142
|
-
if (
|
|
174
|
+
if (editingTagId) {
|
|
143
175
|
await request({
|
|
144
|
-
url: `/tag/${
|
|
176
|
+
url: `/tag/${editingTagId}`,
|
|
145
177
|
method: 'PATCH',
|
|
146
178
|
data: payload,
|
|
147
179
|
});
|
|
@@ -155,7 +187,7 @@ export default function TagPage() {
|
|
|
155
187
|
toast.success(t('successCreate'));
|
|
156
188
|
}
|
|
157
189
|
|
|
158
|
-
|
|
190
|
+
setIsSheetOpen(false);
|
|
159
191
|
await refetchTag();
|
|
160
192
|
await refetchStats();
|
|
161
193
|
} catch (error) {
|
|
@@ -181,20 +213,19 @@ export default function TagPage() {
|
|
|
181
213
|
|
|
182
214
|
const handleSearchChange = (value: string): void => {
|
|
183
215
|
setSearchTerm(value);
|
|
216
|
+
setPage(1);
|
|
184
217
|
};
|
|
185
218
|
|
|
186
219
|
useEffect(() => {
|
|
187
|
-
refetchTag();
|
|
188
220
|
refetchStats();
|
|
189
|
-
}, [
|
|
221
|
+
}, [isSheetOpen, refetchStats]);
|
|
222
|
+
|
|
223
|
+
const isNewTag = editingTagId === null;
|
|
190
224
|
|
|
191
225
|
return (
|
|
192
|
-
<
|
|
226
|
+
<Page>
|
|
193
227
|
<PageHeader
|
|
194
|
-
breadcrumbs={[
|
|
195
|
-
{ label: 'Home', href: '/' },
|
|
196
|
-
{ label: t('description') },
|
|
197
|
-
]}
|
|
228
|
+
breadcrumbs={[{ label: 'Home', href: '/' }, { label: t('title') }]}
|
|
198
229
|
actions={[
|
|
199
230
|
{
|
|
200
231
|
label: t('newTag'),
|
|
@@ -206,142 +237,128 @@ export default function TagPage() {
|
|
|
206
237
|
description={t('description')}
|
|
207
238
|
/>
|
|
208
239
|
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
</p>
|
|
236
|
-
<p className="text-2xl font-bold">{statsData?.active || 0}</p>
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
</CardContent>
|
|
240
|
-
</Card>
|
|
241
|
-
|
|
242
|
-
<Card className="transition-shadow hover:shadow-md p-0">
|
|
243
|
-
<CardContent className="p-4">
|
|
244
|
-
<div className="flex items-center space-x-3">
|
|
245
|
-
<div className="rounded-full bg-gray-100 p-2 dark:bg-gray-900">
|
|
246
|
-
<TagIcon className="h-6 w-6 text-gray-600 dark:text-gray-400" />
|
|
247
|
-
</div>
|
|
248
|
-
<div>
|
|
249
|
-
<p className="text-sm font-medium text-muted-foreground">
|
|
250
|
-
{t('inactiveTags')}
|
|
251
|
-
</p>
|
|
252
|
-
<p className="text-2xl font-bold">{statsData?.inactive || 0}</p>
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
</CardContent>
|
|
256
|
-
</Card>
|
|
257
|
-
</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
|
+
/>
|
|
258
266
|
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
/>
|
|
266
|
-
</div>
|
|
267
|
+
<SearchBar
|
|
268
|
+
searchQuery={searchTerm}
|
|
269
|
+
onSearchChange={handleSearchChange}
|
|
270
|
+
onSearch={() => refetchTag()}
|
|
271
|
+
placeholder={t('searchPlaceholder')}
|
|
272
|
+
/>
|
|
267
273
|
|
|
268
|
-
<div
|
|
269
|
-
{
|
|
270
|
-
<
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
+
/>
|
|
281
290
|
) : (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
<
|
|
290
|
-
<div
|
|
291
|
-
className="
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
<
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
</
|
|
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>
|
|
304
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>
|
|
305
357
|
</div>
|
|
306
|
-
</
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
size="icon"
|
|
311
|
-
onClick={() => handleEditTag(tag)}
|
|
312
|
-
>
|
|
313
|
-
<Edit className="h-4 w-4" />
|
|
314
|
-
</Button>
|
|
315
|
-
<AlertDialog>
|
|
316
|
-
<AlertDialogTrigger asChild>
|
|
317
|
-
<Button variant="ghost" size="icon">
|
|
318
|
-
<Trash2 className="h-4 w-4 text-destructive" />
|
|
319
|
-
</Button>
|
|
320
|
-
</AlertDialogTrigger>
|
|
321
|
-
<AlertDialogContent>
|
|
322
|
-
<AlertDialogHeader>
|
|
323
|
-
<AlertDialogTitle>
|
|
324
|
-
{t('confirmDelete')}
|
|
325
|
-
</AlertDialogTitle>
|
|
326
|
-
<AlertDialogDescription>
|
|
327
|
-
{t('deleteDescription')}
|
|
328
|
-
</AlertDialogDescription>
|
|
329
|
-
</AlertDialogHeader>
|
|
330
|
-
<AlertDialogFooter>
|
|
331
|
-
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
|
|
332
|
-
<AlertDialogAction
|
|
333
|
-
onClick={() => handleDeleteTag(tag.id)}
|
|
334
|
-
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
335
|
-
>
|
|
336
|
-
{t('delete')}
|
|
337
|
-
</AlertDialogAction>
|
|
338
|
-
</AlertDialogFooter>
|
|
339
|
-
</AlertDialogContent>
|
|
340
|
-
</AlertDialog>
|
|
341
|
-
</div>
|
|
342
|
-
</CardContent>
|
|
343
|
-
</Card>
|
|
344
|
-
))
|
|
358
|
+
</CardContent>
|
|
359
|
+
</Card>
|
|
360
|
+
))}
|
|
361
|
+
</div>
|
|
345
362
|
)}
|
|
346
363
|
</div>
|
|
347
364
|
|
|
@@ -351,89 +368,102 @@ export default function TagPage() {
|
|
|
351
368
|
totalItems={total}
|
|
352
369
|
onPageChange={setPage}
|
|
353
370
|
onPageSizeChange={setPageSize}
|
|
354
|
-
pageSizeOptions={[
|
|
371
|
+
pageSizeOptions={[...PAGE_SIZE_OPTIONS]}
|
|
355
372
|
/>
|
|
356
373
|
|
|
357
|
-
<
|
|
358
|
-
<
|
|
359
|
-
<
|
|
360
|
-
<
|
|
361
|
-
|
|
362
|
-
</DialogTitle>
|
|
363
|
-
<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>
|
|
364
379
|
{isNewTag ? t('createDescription') : t('editDescription')}
|
|
365
|
-
</
|
|
366
|
-
</
|
|
380
|
+
</SheetDescription>
|
|
381
|
+
</SheetHeader>
|
|
367
382
|
|
|
368
|
-
<
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
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
|
+
)}
|
|
378
400
|
/>
|
|
379
|
-
</div>
|
|
380
401
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
+
/>
|
|
402
427
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
+
/>
|
|
421
456
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
</Button>
|
|
434
|
-
</DialogFooter>
|
|
435
|
-
</DialogContent>
|
|
436
|
-
</Dialog>
|
|
437
|
-
</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>
|
|
438
468
|
);
|
|
439
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.
|
|
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/
|
|
14
|
+
"@hed-hog/api": "0.0.4",
|
|
15
15
|
"@hed-hog/api-locale": "0.0.13",
|
|
16
|
-
"@hed-hog/
|
|
16
|
+
"@hed-hog/core": "0.0.285"
|
|
17
17
|
},
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|