@hed-hog/content 0.0.186 → 0.0.191

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.
@@ -0,0 +1,747 @@
1
+ 'use client';
2
+
3
+ import { PageHeader } from '@/components/entity-list';
4
+ import { RichTextEditor } from '@/components/rich-text-editor';
5
+ import {
6
+ AlertDialog,
7
+ AlertDialogAction,
8
+ AlertDialogCancel,
9
+ AlertDialogContent,
10
+ AlertDialogDescription,
11
+ AlertDialogFooter,
12
+ AlertDialogHeader,
13
+ AlertDialogTitle,
14
+ AlertDialogTrigger,
15
+ } from '@/components/ui/alert-dialog';
16
+ import { Badge } from '@/components/ui/badge';
17
+ import { Button } from '@/components/ui/button';
18
+ import { Card, CardContent } from '@/components/ui/card';
19
+ import {
20
+ Dialog,
21
+ DialogContent,
22
+ DialogDescription,
23
+ DialogFooter,
24
+ DialogHeader,
25
+ DialogTitle,
26
+ } from '@/components/ui/dialog';
27
+ import {
28
+ Form,
29
+ FormControl,
30
+ FormField,
31
+ FormItem,
32
+ FormLabel,
33
+ FormMessage,
34
+ } from '@/components/ui/form';
35
+ import { Input } from '@/components/ui/input';
36
+ import { Label } from '@/components/ui/label';
37
+ import {
38
+ Select,
39
+ SelectContent,
40
+ SelectItem,
41
+ SelectTrigger,
42
+ SelectValue,
43
+ } from '@/components/ui/select';
44
+ import { useDebounce } from '@/hooks/use-debounce';
45
+ import { formatDateTime } from '@/lib/format-date';
46
+ import { cn } from '@/lib/utils';
47
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
48
+ import { zodResolver } from '@hookform/resolvers/zod';
49
+ import {
50
+ Calendar,
51
+ Edit,
52
+ Eye,
53
+ FileText,
54
+ Globe,
55
+ Languages,
56
+ Loader2,
57
+ Plus,
58
+ Search,
59
+ Trash2,
60
+ } from 'lucide-react';
61
+ import { useTranslations } from 'next-intl';
62
+ import { useEffect, useState } from 'react';
63
+ import { useForm } from 'react-hook-form';
64
+ import { toast } from 'sonner';
65
+ import { z } from 'zod';
66
+
67
+ type Content = {
68
+ id?: number;
69
+ content_id?: number;
70
+ slug: string;
71
+ status: 'draft' | 'published';
72
+ title?: string;
73
+ body?: string;
74
+ created_at?: string;
75
+ updated_at?: string;
76
+ content_locale?: Array<{
77
+ title: string;
78
+ body: string;
79
+ locale?: {
80
+ code: string;
81
+ name: string;
82
+ };
83
+ }>;
84
+ available_locales?: Array<{
85
+ id: number;
86
+ code: string;
87
+ name: string;
88
+ }>;
89
+ };
90
+
91
+ type PaginationResult<T> = {
92
+ data: T[];
93
+ total: number;
94
+ page: number;
95
+ pageSize: number;
96
+ lastPage: number;
97
+ prev: number | null;
98
+ next: number | null;
99
+ };
100
+
101
+ type Stats = {
102
+ total: number;
103
+ totalPublished: number;
104
+ totalDraft: number;
105
+ totalEnabledLanguages: number;
106
+ };
107
+
108
+ type Locale = {
109
+ id: number;
110
+ code: string;
111
+ name: string;
112
+ enabled: boolean;
113
+ };
114
+
115
+ export default function ContentsPage() {
116
+ const [searchTerm, setSearchTerm] = useState('');
117
+ const debouncedSearch = useDebounce(searchTerm);
118
+ const [isFormDialogOpen, setIsFormDialogOpen] = useState(false);
119
+ const [editingContentId, setEditingContentId] = useState<number | null>(null);
120
+ const [isSubmitting, setIsSubmitting] = useState(false);
121
+ const [selectedLocale, setSelectedLocale] = useState<string>('');
122
+ const { request, currentLocaleCode, getSettingValue } = useApp();
123
+ const t = useTranslations('content.ContentPage');
124
+
125
+ const statusOptions = [
126
+ { value: 'draft', label: t('statusDraft'), color: 'bg-gray-500' },
127
+ { value: 'published', label: t('statusPublished'), color: 'bg-green-500' },
128
+ ];
129
+
130
+ const contentSchema = z.object({
131
+ slug: z.string().min(1, t('errorSlugRequired')),
132
+ status: z.enum(['draft', 'published']),
133
+ title: z.string().min(1, t('errorTitleRequired')),
134
+ body: z.string().min(1, t('errorContentRequired')),
135
+ });
136
+
137
+ const { data: locales = [] } = useQuery<Locale[]>({
138
+ queryKey: ['locales'],
139
+ queryFn: async () => {
140
+ const response = await request<PaginationResult<Locale>>({
141
+ url: '/locale',
142
+ params: {
143
+ pageSize: 100,
144
+ },
145
+ });
146
+ return response.data?.data?.filter((l: Locale) => l.enabled) || [];
147
+ },
148
+ });
149
+
150
+ const form = useForm<z.infer<typeof contentSchema>>({
151
+ resolver: zodResolver(contentSchema),
152
+ defaultValues: {
153
+ slug: '',
154
+ status: 'draft',
155
+ title: '',
156
+ body: '',
157
+ },
158
+ });
159
+
160
+ const { data: stats, refetch: refetchStats } = useQuery<Stats>({
161
+ queryKey: ['content-stats', currentLocaleCode],
162
+ queryFn: async () => {
163
+ const response = await request({
164
+ url: '/content/stats',
165
+ });
166
+ return response.data as Stats;
167
+ },
168
+ });
169
+
170
+ const { data: contents, refetch: refetch } = useQuery<
171
+ PaginationResult<Content>
172
+ >({
173
+ queryKey: ['content', currentLocaleCode],
174
+ queryFn: async () => {
175
+ const response = await request({
176
+ url: '/content',
177
+ params: {
178
+ search: debouncedSearch,
179
+ },
180
+ });
181
+ return response.data as PaginationResult<Content>;
182
+ },
183
+ initialData: {
184
+ data: [],
185
+ total: 0,
186
+ page: 1,
187
+ pageSize: 10,
188
+ lastPage: 1,
189
+ prev: null,
190
+ next: null,
191
+ },
192
+ });
193
+
194
+ const getStatusBadge = (status: string) => {
195
+ const statusOption = statusOptions.find((s) => s.value === status);
196
+ return statusOption || statusOptions[0];
197
+ };
198
+
199
+ const handleNewContent = (): void => {
200
+ form.reset({
201
+ slug: '',
202
+ status: 'draft',
203
+ title: '',
204
+ body: '',
205
+ });
206
+ setSelectedLocale(currentLocaleCode);
207
+ setEditingContentId(null);
208
+ setIsFormDialogOpen(true);
209
+ };
210
+
211
+ const handleEditContent = async (content: Content): Promise<void> => {
212
+ try {
213
+ const response = await request<any>({
214
+ url: `/content/${content.content_id || content.id}`,
215
+ method: 'GET',
216
+ });
217
+
218
+ const fullContent = response.data;
219
+ const allLocalesData: Record<string, { title: string; body: string }> =
220
+ fullContent.locales || {};
221
+
222
+ if (Object.keys(allLocalesData).length === 0) {
223
+ allLocalesData[currentLocaleCode] = {
224
+ title: '',
225
+ body: '',
226
+ };
227
+ }
228
+
229
+ const initialLocale = allLocalesData[currentLocaleCode]
230
+ ? currentLocaleCode
231
+ : Object.keys(allLocalesData)[0] || currentLocaleCode;
232
+
233
+ if (!allLocalesData[initialLocale]) {
234
+ allLocalesData[initialLocale] = {
235
+ title: '',
236
+ body: '',
237
+ };
238
+ }
239
+
240
+ setSelectedLocale(initialLocale);
241
+ (form as any).localesData = allLocalesData;
242
+ form.reset({
243
+ slug: fullContent.slug || '',
244
+ status: fullContent.status || 'draft',
245
+ title: allLocalesData[initialLocale]?.title || '',
246
+ body: allLocalesData[initialLocale]?.body || '',
247
+ });
248
+
249
+ setEditingContentId(content.content_id || content.id!);
250
+ setIsFormDialogOpen(true);
251
+ } catch (error) {
252
+ console.error('Error loading content:', error);
253
+ toast.error(t('errorLoad'));
254
+ }
255
+ };
256
+
257
+ const onSubmit = async (values: z.infer<typeof contentSchema>) => {
258
+ setIsSubmitting(true);
259
+ try {
260
+ const payload = {
261
+ slug: values.slug,
262
+ status: values.status,
263
+ locale: {
264
+ [selectedLocale]: {
265
+ title: values.title,
266
+ body: values.body,
267
+ },
268
+ },
269
+ };
270
+
271
+ if (editingContentId) {
272
+ await request({
273
+ url: `/content/${editingContentId}`,
274
+ method: 'PATCH',
275
+ data: payload,
276
+ });
277
+ toast.success(t('successUpdate'));
278
+ } else {
279
+ await request({
280
+ url: '/content',
281
+ method: 'POST',
282
+ data: payload,
283
+ });
284
+ toast.success(t('successCreate'));
285
+ }
286
+
287
+ setIsFormDialogOpen(false);
288
+ refetch();
289
+ refetchStats();
290
+ } catch (error: any) {
291
+ toast.error(
292
+ error?.message || t(editingContentId ? 'errorUpdate' : 'errorCreate')
293
+ );
294
+ } finally {
295
+ setIsSubmitting(false);
296
+ }
297
+ };
298
+
299
+ const handleDeleteContent = (contentId: number): void => {
300
+ request({
301
+ url: `/content/${contentId}`,
302
+ method: 'DELETE',
303
+ }).then(() => {
304
+ toast.success(t('successDelete'));
305
+ refetch();
306
+ refetchStats();
307
+ });
308
+ };
309
+
310
+ const handleSearchChange = (value: string): void => {
311
+ setSearchTerm(value);
312
+ };
313
+
314
+ useEffect(() => {
315
+ refetch();
316
+ }, [debouncedSearch]);
317
+
318
+ return (
319
+ <div className="flex flex-col h-screen px-4">
320
+ <PageHeader
321
+ breadcrumbs={[
322
+ { label: 'Home', href: '/' },
323
+ { label: t('description') },
324
+ ]}
325
+ title={t('title')}
326
+ description={t('description')}
327
+ actions={[
328
+ {
329
+ label: t('buttonNew'),
330
+ onClick: () => handleNewContent(),
331
+ variant: 'default',
332
+ },
333
+ ]}
334
+ />
335
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
336
+ <Card className="transition-shadow hover:shadow-md">
337
+ <CardContent className="p-6">
338
+ <div className="flex items-center space-x-3">
339
+ <FileText className="h-10 w-10 text-blue-500" />
340
+ <div>
341
+ <p className="text-sm font-medium text-muted-foreground">
342
+ {t('statsTotalPages')}
343
+ </p>
344
+ <p className="text-2xl font-bold">{stats?.total}</p>
345
+ </div>
346
+ </div>
347
+ </CardContent>
348
+ </Card>
349
+
350
+ <Card className="transition-shadow hover:shadow-md">
351
+ <CardContent className="p-6">
352
+ <div className="flex items-center space-x-3">
353
+ <Eye className="h-10 w-10 text-green-500" />
354
+ <div>
355
+ <p className="text-sm font-medium text-muted-foreground">
356
+ {t('statsPublished')}
357
+ </p>
358
+ <p className="text-2xl font-bold">{stats?.totalPublished}</p>
359
+ </div>
360
+ </div>
361
+ </CardContent>
362
+ </Card>
363
+
364
+ <Card className="transition-shadow hover:shadow-md">
365
+ <CardContent className="p-6">
366
+ <div className="flex items-center space-x-3">
367
+ <Edit className="h-10 w-10 text-orange-500" />
368
+ <div>
369
+ <p className="text-sm font-medium text-muted-foreground">
370
+ {t('statsDrafts')}
371
+ </p>
372
+ <p className="text-2xl font-bold">{stats?.totalDraft}</p>
373
+ </div>
374
+ </div>
375
+ </CardContent>
376
+ </Card>
377
+
378
+ <Card className="transition-shadow hover:shadow-md">
379
+ <CardContent className="p-6">
380
+ <div className="flex items-center space-x-3">
381
+ <Languages className="h-10 w-10 text-purple-500" />
382
+ <div>
383
+ <p className="text-sm font-medium text-muted-foreground">
384
+ {t('statsActiveLanguages')}
385
+ </p>
386
+ <p className="text-2xl font-bold">
387
+ {stats?.totalEnabledLanguages}
388
+ </p>
389
+ </div>
390
+ </div>
391
+ </CardContent>
392
+ </Card>
393
+ </div>
394
+
395
+ <div className="relative my-4">
396
+ <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
397
+ <Input
398
+ placeholder={t('searchPlaceholder')}
399
+ value={searchTerm}
400
+ onChange={(e) => handleSearchChange(e.target.value)}
401
+ className="pl-10"
402
+ />
403
+ </div>
404
+
405
+ <div className="space-y-4">
406
+ {contents.data.length > 0 ? (
407
+ <div className="grid gap-4">
408
+ {contents.data.map((content: Content) => (
409
+ <Card
410
+ key={content.id}
411
+ className="overflow-hidden transition-all duration-200 hover:border-primary/20 hover:shadow-md"
412
+ >
413
+ <CardContent className="p-6">
414
+ <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
415
+ <div className="flex-1 space-y-3">
416
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
417
+ <div className="space-y-1">
418
+ <div className="flex items-center space-x-2">
419
+ <h3 className="text-lg font-semibold">
420
+ /{content.slug}
421
+ </h3>
422
+ {content.status && (
423
+ <Badge
424
+ className={cn(
425
+ 'text-white',
426
+ getStatusBadge(content.status)?.color ||
427
+ 'bg-gray-500'
428
+ )}
429
+ >
430
+ {getStatusBadge(content.status)?.label || 'N/A'}
431
+ </Badge>
432
+ )}
433
+ </div>
434
+ <div className="flex items-center space-x-2">
435
+ {content.available_locales?.length &&
436
+ content.available_locales.map((al) => (
437
+ <Badge
438
+ variant="outline"
439
+ className="text-xs"
440
+ key={al.id}
441
+ >
442
+ <Globe className="mr-1 h-3 w-3" />
443
+ {al.code.toUpperCase()}
444
+ </Badge>
445
+ ))}
446
+ </div>
447
+ </div>
448
+ </div>
449
+
450
+ <div className="space-y-2">
451
+ <div className="flex items-center space-x-4 text-xs text-muted-foreground">
452
+ <div className="flex items-center space-x-1">
453
+ <Calendar className="h-3 w-3" />
454
+ <span>
455
+ {t('created')}{' '}
456
+ {content.created_at
457
+ ? formatDateTime(
458
+ content.created_at,
459
+ getSettingValue,
460
+ currentLocaleCode
461
+ )
462
+ : 'N/A'}
463
+ </span>
464
+ </div>
465
+ <div className="flex items-center space-x-1">
466
+ <Calendar className="h-3 w-3" />
467
+ <span>
468
+ {t('updated')}{' '}
469
+ {content.updated_at
470
+ ? formatDateTime(
471
+ content.updated_at,
472
+ getSettingValue,
473
+ currentLocaleCode
474
+ )
475
+ : 'N/A'}
476
+ </span>
477
+ </div>
478
+ </div>
479
+ </div>
480
+ </div>
481
+
482
+ <div className="flex flex-wrap gap-2 lg:flex-col lg:items-end">
483
+ <Button
484
+ variant="outline"
485
+ size="sm"
486
+ onClick={() => handleEditContent(content)}
487
+ className="transition-colors hover:border-blue-200 hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-950"
488
+ >
489
+ <Edit className="mr-1 h-4 w-4" />
490
+ {t('buttonEdit')}
491
+ </Button>
492
+
493
+ <AlertDialog>
494
+ <AlertDialogTrigger asChild>
495
+ <Button
496
+ variant="outline"
497
+ size="sm"
498
+ className="bg-transparent transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950"
499
+ >
500
+ <Trash2 className="mr-1 h-4 w-4" />
501
+ {t('buttonDelete')}
502
+ </Button>
503
+ </AlertDialogTrigger>
504
+ <AlertDialogContent>
505
+ <AlertDialogHeader>
506
+ <AlertDialogTitle>
507
+ {t('alertDeleteTitle')}
508
+ </AlertDialogTitle>
509
+ <AlertDialogDescription>
510
+ {t('alertDeleteDescription', {
511
+ slug: content.slug,
512
+ })}
513
+ </AlertDialogDescription>
514
+ </AlertDialogHeader>
515
+ <AlertDialogFooter>
516
+ <AlertDialogCancel>
517
+ {t('alertDeleteCancel')}
518
+ </AlertDialogCancel>
519
+ <AlertDialogAction
520
+ onClick={() => {
521
+ if (content.id) {
522
+ handleDeleteContent(
523
+ content.content_id || content.id
524
+ );
525
+ }
526
+ }}
527
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
528
+ >
529
+ {t('alertDeleteConfirm')}
530
+ </AlertDialogAction>
531
+ </AlertDialogFooter>
532
+ </AlertDialogContent>
533
+ </AlertDialog>
534
+ </div>
535
+ </div>
536
+ </CardContent>
537
+ </Card>
538
+ ))}
539
+ </div>
540
+ ) : (
541
+ <Card>
542
+ <CardContent className="p-12 text-center">
543
+ <div className="flex flex-col items-center space-y-4">
544
+ <FileText className="h-12 w-12 text-muted-foreground" />
545
+ <div>
546
+ <h3 className="text-lg font-semibold">{t('emptyTitle')}</h3>
547
+ <p className="text-muted-foreground">
548
+ {t('emptyDescription')}
549
+ </p>
550
+ </div>
551
+ <Button onClick={handleNewContent}>
552
+ <Plus className="mr-2 h-4 w-4" />
553
+ {t('emptyButtonCreate')}
554
+ </Button>
555
+ </div>
556
+ </CardContent>
557
+ </Card>
558
+ )}
559
+ </div>
560
+ <Dialog open={isFormDialogOpen} onOpenChange={setIsFormDialogOpen}>
561
+ <DialogContent className="max-h-[90vh] max-w-4xl overflow-y-auto">
562
+ <DialogHeader>
563
+ <DialogTitle>
564
+ {editingContentId
565
+ ? t('dialogFormTitleEdit')
566
+ : t('dialogFormTitleCreate')}
567
+ </DialogTitle>
568
+ <DialogDescription>
569
+ {editingContentId
570
+ ? t('dialogFormDescriptionEdit')
571
+ : t('dialogFormDescriptionCreate')}
572
+ </DialogDescription>
573
+ </DialogHeader>
574
+
575
+ <Form {...form}>
576
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
577
+ <div className="grid gap-4 sm:grid-cols-2">
578
+ <FormField
579
+ control={form.control}
580
+ name="slug"
581
+ render={({ field }) => (
582
+ <FormItem>
583
+ <FormLabel>{t('formLabelSlug')}</FormLabel>
584
+ <FormControl>
585
+ <Input
586
+ placeholder={t('formPlaceholderSlug')}
587
+ {...field}
588
+ disabled={isSubmitting}
589
+ />
590
+ </FormControl>
591
+ <FormMessage />
592
+ </FormItem>
593
+ )}
594
+ />
595
+
596
+ <FormField
597
+ control={form.control}
598
+ name="status"
599
+ render={({ field }) => (
600
+ <FormItem>
601
+ <FormLabel>{t('formLabelStatus')}</FormLabel>
602
+ <Select
603
+ onValueChange={field.onChange}
604
+ value={field.value}
605
+ disabled={isSubmitting}
606
+ >
607
+ <FormControl>
608
+ <SelectTrigger className="w-full">
609
+ <SelectValue
610
+ placeholder={t('formPlaceholderStatus')}
611
+ />
612
+ </SelectTrigger>
613
+ </FormControl>
614
+ <SelectContent>
615
+ {statusOptions.map((option) => (
616
+ <SelectItem key={option.value} value={option.value}>
617
+ {option.label}
618
+ </SelectItem>
619
+ ))}
620
+ </SelectContent>
621
+ </Select>
622
+ <FormMessage />
623
+ </FormItem>
624
+ )}
625
+ />
626
+ </div>
627
+
628
+ {editingContentId && (
629
+ <div className="space-y-2">
630
+ <Label>{t('formLabelLanguage')}</Label>
631
+ <Select
632
+ value={selectedLocale}
633
+ onValueChange={(newLocale) => {
634
+ const currentValues = form.getValues();
635
+ const localesData = (form as any).localesData || {};
636
+
637
+ if (selectedLocale) {
638
+ localesData[selectedLocale] = {
639
+ title: currentValues.title,
640
+ body: currentValues.body,
641
+ };
642
+ }
643
+
644
+ const newLocaleData = localesData[newLocale] || {
645
+ title: '',
646
+ body: '',
647
+ };
648
+
649
+ form.setValue('title', newLocaleData.title);
650
+ form.setValue('body', newLocaleData.body);
651
+ (form as any).localesData = localesData;
652
+ setSelectedLocale(newLocale);
653
+ }}
654
+ disabled={isSubmitting}
655
+ >
656
+ <SelectTrigger className="w-full">
657
+ <SelectValue placeholder={t('formPlaceholderLanguage')} />
658
+ </SelectTrigger>
659
+ <SelectContent>
660
+ {locales.map((locale) => (
661
+ <SelectItem key={locale.code} value={locale.code}>
662
+ <div className="flex items-center">
663
+ <Globe className="mr-2 h-4 w-4" />
664
+ {locale.name}
665
+ </div>
666
+ </SelectItem>
667
+ ))}
668
+ </SelectContent>
669
+ </Select>
670
+ </div>
671
+ )}
672
+
673
+ <div className="space-y-4">
674
+ <FormField
675
+ control={form.control}
676
+ name="title"
677
+ render={({ field }) => (
678
+ <FormItem>
679
+ <FormLabel>
680
+ {t('formLabelTitle', {
681
+ language:
682
+ locales.find((l) => l.code === selectedLocale)
683
+ ?.name || '',
684
+ })}
685
+ </FormLabel>
686
+ <FormControl>
687
+ <Input
688
+ placeholder={t('formPlaceholderTitle', {
689
+ language:
690
+ locales.find((l) => l.code === selectedLocale)
691
+ ?.name || '',
692
+ })}
693
+ {...field}
694
+ disabled={isSubmitting}
695
+ />
696
+ </FormControl>
697
+ <FormMessage />
698
+ </FormItem>
699
+ )}
700
+ />
701
+
702
+ <FormField
703
+ control={form.control}
704
+ name="body"
705
+ render={({ field }) => (
706
+ <FormItem>
707
+ <FormLabel>
708
+ {t('formLabelContent', {
709
+ language:
710
+ locales.find((l) => l.code === selectedLocale)
711
+ ?.name || '',
712
+ })}
713
+ </FormLabel>
714
+ <FormControl>
715
+ <RichTextEditor className="max-w-[845px]" {...field} />
716
+ </FormControl>
717
+ <FormMessage />
718
+ </FormItem>
719
+ )}
720
+ />
721
+ </div>
722
+
723
+ <DialogFooter>
724
+ <Button
725
+ type="button"
726
+ variant="outline"
727
+ onClick={() => setIsFormDialogOpen(false)}
728
+ disabled={isSubmitting}
729
+ >
730
+ {t('formButtonCancel')}
731
+ </Button>
732
+ <Button type="submit" disabled={isSubmitting}>
733
+ {isSubmitting && (
734
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
735
+ )}
736
+ {editingContentId
737
+ ? t('formButtonUpdate')
738
+ : t('formButtonCreate')}
739
+ </Button>
740
+ </DialogFooter>
741
+ </form>
742
+ </Form>
743
+ </DialogContent>
744
+ </Dialog>
745
+ </div>
746
+ );
747
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "ContentPage": {
3
+ "title": "Contents",
4
+ "description": "Manage website content in multiple languages",
5
+ "buttonNew": "New Content",
6
+ "statsTotalPages": "Total Pages",
7
+ "statsPublished": "Published",
8
+ "statsDrafts": "Drafts",
9
+ "statsActiveLanguages": "Active Languages",
10
+ "searchPlaceholder": "Search by slug, title or content...",
11
+ "filterAllStatus": "All Statuses",
12
+ "statusDraft": "Draft",
13
+ "statusPublished": "Published",
14
+ "buttonView": "View",
15
+ "buttonEdit": "Edit",
16
+ "buttonDelete": "Delete",
17
+ "alertDeleteTitle": "Confirm Deletion",
18
+ "alertDeleteDescription": "Are you sure you want to delete the page \"/{slug}\"? This action cannot be undone.",
19
+ "alertDeleteCancel": "Cancel",
20
+ "alertDeleteConfirm": "Delete",
21
+ "emptyTitle": "No content found",
22
+ "emptyDescription": "Try adjusting the filters or create new content.",
23
+ "emptyButtonCreate": "Create First Content",
24
+ "dialogViewTitle": "View Content: /{slug}",
25
+ "dialogViewDescription": "View content in all available languages",
26
+ "dialogViewLabelTitle": "Title",
27
+ "dialogViewNoTitle": "No title",
28
+ "dialogViewLabelContent": "Content",
29
+ "dialogViewNoContent": "No content",
30
+ "dialogViewButtonClose": "Close",
31
+ "dialogViewButtonEdit": "Edit",
32
+ "dialogFormTitleCreate": "Create New Content",
33
+ "dialogFormTitleEdit": "Edit Content",
34
+ "dialogFormDescriptionCreate": "Fill in the details of the new content",
35
+ "dialogFormDescriptionEdit": "Update content details",
36
+ "formLabelSlug": "Slug",
37
+ "formPlaceholderSlug": "example-page",
38
+ "formLabelStatus": "Status",
39
+ "formPlaceholderStatus": "Select status",
40
+ "formLabelLanguage": "Language",
41
+ "formPlaceholderLanguage": "Select language",
42
+ "formLabelContentByLanguage": "Content by Language",
43
+ "formLabelTitle": "Title ({language})",
44
+ "formPlaceholderTitle": "Enter title in {language}",
45
+ "formLabelContent": "Content ({language})",
46
+ "formPlaceholderContent": "Enter content in {language}",
47
+ "formButtonCancel": "Cancel",
48
+ "formButtonCreate": "Create",
49
+ "formButtonUpdate": "Update",
50
+ "errorSlugRequired": "Slug is required",
51
+ "errorTitleRequired": "Title is required",
52
+ "errorContentRequired": "Content is required",
53
+ "successCreate": "Content created successfully",
54
+ "successUpdate": "Content updated successfully",
55
+ "successDelete": "Content deleted successfully",
56
+ "errorCreate": "Error creating content",
57
+ "errorUpdate": "Error updating content",
58
+ "errorLoad": "Error loading content",
59
+ "created": "Created:",
60
+ "updated": "Updated:"
61
+ }
62
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "ContentPage": {
3
+ "title": "Conteúdos",
4
+ "description": "Gerencie o conteúdo das páginas do site em múltiplos idiomas",
5
+ "buttonNew": "Novo Conteúdo",
6
+ "statsTotalPages": "Total de Páginas",
7
+ "statsPublished": "Publicadas",
8
+ "statsDrafts": "Rascunhos",
9
+ "statsActiveLanguages": "Idiomas Ativos",
10
+ "searchPlaceholder": "Buscar por slug, título ou conteúdo...",
11
+ "filterAllStatus": "Todos os Status",
12
+ "statusDraft": "Rascunho",
13
+ "statusPublished": "Publicado",
14
+ "buttonView": "Ver",
15
+ "buttonEdit": "Editar",
16
+ "buttonDelete": "Excluir",
17
+ "alertDeleteTitle": "Confirmar Exclusão",
18
+ "alertDeleteDescription": "Tem certeza que deseja excluir a página \"/{slug}\"? Esta ação não pode ser desfeita.",
19
+ "alertDeleteCancel": "Cancelar",
20
+ "alertDeleteConfirm": "Excluir",
21
+ "emptyTitle": "Nenhum conteúdo encontrado",
22
+ "emptyDescription": "Tente ajustar os filtros ou criar um novo conteúdo.",
23
+ "emptyButtonCreate": "Criar Primeiro Conteúdo",
24
+ "dialogViewTitle": "Visualizar Conteúdo: /{slug}",
25
+ "dialogViewDescription": "Visualize o conteúdo em todos os idiomas disponíveis",
26
+ "dialogViewLabelTitle": "Título",
27
+ "dialogViewNoTitle": "Sem título",
28
+ "dialogViewLabelContent": "Conteúdo",
29
+ "dialogViewNoContent": "Sem conteúdo",
30
+ "dialogViewButtonClose": "Fechar",
31
+ "dialogViewButtonEdit": "Editar",
32
+ "dialogFormTitleCreate": "Criar Novo Conteúdo",
33
+ "dialogFormTitleEdit": "Editar Conteúdo",
34
+ "dialogFormDescriptionCreate": "Preencha os detalhes do novo conteúdo",
35
+ "dialogFormDescriptionEdit": "Atualize os detalhes do conteúdo",
36
+ "formLabelSlug": "Slug",
37
+ "formPlaceholderSlug": "pagina-exemplo",
38
+ "formLabelStatus": "Status",
39
+ "formPlaceholderStatus": "Selecione o status",
40
+ "formLabelLanguage": "Idioma",
41
+ "formPlaceholderLanguage": "Selecione o idioma",
42
+ "formLabelContentByLanguage": "Conteúdo por Idioma",
43
+ "formLabelTitle": "Título ({language})",
44
+ "formPlaceholderTitle": "Digite o título em {language}",
45
+ "formLabelContent": "Conteúdo ({language})",
46
+ "formPlaceholderContent": "Digite o conteúdo em {language}",
47
+ "formButtonCancel": "Cancelar",
48
+ "formButtonCreate": "Criar",
49
+ "formButtonUpdate": "Atualizar",
50
+ "errorSlugRequired": "Slug é obrigatório",
51
+ "errorTitleRequired": "Título é obrigatório",
52
+ "errorContentRequired": "Conteúdo é obrigatório",
53
+ "successCreate": "Conteúdo criado com sucesso",
54
+ "successUpdate": "Conteúdo atualizado com sucesso",
55
+ "successDelete": "Conteúdo excluído com sucesso",
56
+ "errorCreate": "Erro ao criar conteúdo",
57
+ "errorUpdate": "Erro ao atualizar conteúdo",
58
+ "errorLoad": "Erro ao carregar conteúdo",
59
+ "created": "Criado:",
60
+ "updated": "Atualizado:"
61
+ }
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/content",
3
- "version": "0.0.186",
3
+ "version": "0.0.191",
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.4",
13
+ "@hed-hog/api": "0.0.3",
13
14
  "@hed-hog/api-pagination": "0.0.5",
14
- "@hed-hog/api-locale": "0.0.11",
15
- "@hed-hog/core": "0.0.186",
16
- "@hed-hog/api": "0.0.3"
15
+ "@hed-hog/core": "0.0.191",
16
+ "@hed-hog/api-locale": "0.0.11"
17
17
  },
18
18
  "exports": {
19
19
  ".": {