@hed-hog/faq 0.0.186 → 0.0.190

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,552 @@
1
+ 'use client';
2
+
3
+ import {
4
+ PageHeader,
5
+ PaginationFooter,
6
+ SearchBar,
7
+ } from '@/components/entity-list';
8
+ import {
9
+ AlertDialog,
10
+ AlertDialogAction,
11
+ AlertDialogCancel,
12
+ AlertDialogContent,
13
+ AlertDialogDescription,
14
+ AlertDialogFooter,
15
+ AlertDialogHeader,
16
+ AlertDialogTitle,
17
+ AlertDialogTrigger,
18
+ } from '@/components/ui/alert-dialog';
19
+ import { Badge } from '@/components/ui/badge';
20
+ import { Button } from '@/components/ui/button';
21
+ import { Card, CardContent } from '@/components/ui/card';
22
+ import {
23
+ Dialog,
24
+ DialogContent,
25
+ DialogDescription,
26
+ DialogFooter,
27
+ DialogHeader,
28
+ DialogTitle,
29
+ } from '@/components/ui/dialog';
30
+ import { Input } from '@/components/ui/input';
31
+ import { Label } from '@/components/ui/label';
32
+ import {
33
+ Select,
34
+ SelectContent,
35
+ SelectItem,
36
+ SelectTrigger,
37
+ SelectValue,
38
+ } from '@/components/ui/select';
39
+ import { Textarea } from '@/components/ui/textarea';
40
+ import { useDebounce } from '@/hooks/use-debounce';
41
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
42
+ import { Edit, Globe, HelpCircle, Plus, Save, Trash2, X } from 'lucide-react';
43
+ import { useTranslations } from 'next-intl';
44
+ import { useEffect, useState } from 'react';
45
+ import { toast } from 'sonner';
46
+
47
+ type PaginationResult<T> = {
48
+ data: T[];
49
+ total: number;
50
+ page: number;
51
+ pageSize: number;
52
+ };
53
+
54
+ type Locale = {
55
+ code: string;
56
+ name: string;
57
+ };
58
+
59
+ type FaqLocale = {
60
+ question: string;
61
+ answer: string;
62
+ };
63
+
64
+ type Faq = {
65
+ question: string;
66
+ answer: string;
67
+ faq_id?: number;
68
+ id?: number;
69
+ locale?: Record<string, FaqLocale>;
70
+ available_locales?: Array<{ code: string; name: string }>;
71
+ };
72
+
73
+ type FaqDetail = {
74
+ faq_id: number;
75
+ id: number;
76
+ faq_locale?: Array<{
77
+ question: string;
78
+ answer: string;
79
+ locale?: {
80
+ code: string;
81
+ };
82
+ }>;
83
+ };
84
+
85
+ export default function FAQPage() {
86
+ const t = useTranslations('faq.Faq');
87
+ const [faqs, setFAQs] = useState<any[]>([]);
88
+ const [selectedFAQ, setSelectedFAQ] = useState<any | null>(null);
89
+ const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
90
+ const [isNewFAQ, setIsNewFAQ] = useState(false);
91
+ const [searchTerm, setSearchTerm] = useState('');
92
+ const debouncedSearch = useDebounce(searchTerm);
93
+ const [page, setPage] = useState(1);
94
+ const [pageSize, setPageSize] = useState(10);
95
+ const [selectedLocale, setSelectedLocale] = useState<string>('');
96
+ const { request, locales, currentLocaleCode } = useApp();
97
+
98
+ useEffect(() => {
99
+ if (currentLocaleCode && !selectedLocale) {
100
+ setSelectedLocale(currentLocaleCode);
101
+ }
102
+ }, [currentLocaleCode]);
103
+
104
+ const {
105
+ data: { data, total },
106
+ refetch: refetchFaq,
107
+ } = useQuery<PaginationResult<Faq>>({
108
+ queryKey: ['faq', debouncedSearch, page, pageSize],
109
+ queryFn: async () => {
110
+ const response = await request({
111
+ url: '/faq',
112
+ params: {
113
+ search: debouncedSearch,
114
+ page,
115
+ pageSize,
116
+ },
117
+ });
118
+ return response.data as PaginationResult<Faq>;
119
+ },
120
+ initialData: {
121
+ data: [],
122
+ total: 0,
123
+ page: 1,
124
+ pageSize: 10,
125
+ },
126
+ });
127
+
128
+ const { data: statsData, refetch: refetchStats } = useQuery<any>({
129
+ queryKey: ['faq-stats'],
130
+ queryFn: async () => {
131
+ const response = await request({
132
+ url: '/faq/stats',
133
+ });
134
+ return response.data;
135
+ },
136
+ });
137
+
138
+ useEffect(() => {
139
+ if (data) {
140
+ setFAQs(data);
141
+ }
142
+ }, [data]);
143
+
144
+ const handleNewFAQ = (): void => {
145
+ const newFAQ: any = {
146
+ locale: {},
147
+ };
148
+
149
+ locales.forEach((locale: Locale) => {
150
+ newFAQ.locale[locale.code] = {
151
+ question: '',
152
+ answer: '',
153
+ };
154
+ });
155
+
156
+ setSelectedFAQ(newFAQ);
157
+ setIsNewFAQ(true);
158
+ setIsEditDialogOpen(true);
159
+ };
160
+
161
+ const handleEditFAQ = async (faq: Faq): Promise<void> => {
162
+ try {
163
+ const response = await request({
164
+ url: `/faq/${faq.faq_id}`,
165
+ method: 'GET',
166
+ });
167
+
168
+ const fullFaq = response.data as FaqDetail;
169
+ const localeData: Record<string, FaqLocale> = {};
170
+ if (fullFaq.faq_locale && Array.isArray(fullFaq.faq_locale)) {
171
+ fullFaq.faq_locale.forEach((fl: any) => {
172
+ const localeCode = locales.find(
173
+ (l: Locale) => l.code === fl.locale?.code
174
+ )?.code;
175
+ if (localeCode) {
176
+ localeData[localeCode] = {
177
+ question: fl.question || '',
178
+ answer: fl.answer || '',
179
+ };
180
+ }
181
+ });
182
+ }
183
+
184
+ locales.forEach((locale: Locale) => {
185
+ if (!localeData[locale.code]) {
186
+ localeData[locale.code] = {
187
+ question: '',
188
+ answer: '',
189
+ };
190
+ }
191
+ });
192
+
193
+ setSelectedFAQ({
194
+ ...fullFaq,
195
+ locale: localeData,
196
+ });
197
+ setIsNewFAQ(false);
198
+ setIsEditDialogOpen(true);
199
+ } catch (error) {
200
+ console.error(error);
201
+ toast.error(t('errorLoading'));
202
+ }
203
+ };
204
+
205
+ const handleSaveFAQ = async () => {
206
+ if (!selectedFAQ || !selectedFAQ.locale) return;
207
+
208
+ const payload = {
209
+ locale: selectedFAQ.locale,
210
+ };
211
+
212
+ try {
213
+ if (selectedFAQ.id || selectedFAQ.faq_id) {
214
+ await request({
215
+ url: `/faq/${selectedFAQ.faq_id || selectedFAQ.id}`,
216
+ method: 'PATCH',
217
+ data: payload,
218
+ });
219
+ toast.success(t('successUpdate'));
220
+ } else {
221
+ await request({
222
+ url: `/faq`,
223
+ method: 'POST',
224
+ data: payload,
225
+ });
226
+ toast.success(t('successCreate'));
227
+ }
228
+
229
+ setIsEditDialogOpen(false);
230
+ await refetchFaq();
231
+ await refetchStats();
232
+ } catch (error) {
233
+ console.error(error);
234
+ toast.error(t('errorSave'));
235
+ }
236
+ };
237
+
238
+ const handleDeleteFAQ = async (faqId: number): Promise<void> => {
239
+ try {
240
+ await request({
241
+ url: `/faq/${faqId}`,
242
+ method: 'DELETE',
243
+ });
244
+ toast.success(t('successDelete'));
245
+ await refetchFaq();
246
+ await refetchStats();
247
+ } catch (error) {
248
+ console.error(error);
249
+ toast.error(t('errorDelete'));
250
+ }
251
+ };
252
+
253
+ const handleSearchChange = (value: string): void => {
254
+ setSearchTerm(value);
255
+ };
256
+
257
+ useEffect(() => {
258
+ refetchFaq();
259
+ refetchStats();
260
+ }, [isEditDialogOpen, debouncedSearch, page, pageSize]);
261
+
262
+ return (
263
+ <div className="flex flex-col h-screen px-4">
264
+ <PageHeader
265
+ breadcrumbs={[{ label: 'Home', href: '/' }, { label: t('title') }]}
266
+ actions={[
267
+ {
268
+ label: t('newQuestion'),
269
+ onClick: () => handleNewFAQ(),
270
+ variant: 'default',
271
+ },
272
+ ]}
273
+ title={t('title')}
274
+ description={t('description')}
275
+ />
276
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
277
+ <Card className="transition-shadow hover:shadow-md p-2">
278
+ <CardContent className="p-4">
279
+ <div className="flex items-center space-x-3">
280
+ <div className="rounded-full bg-blue-100 p-2 dark:bg-blue-900">
281
+ <HelpCircle className="h-6 w-6 text-blue-600 dark:text-blue-400" />
282
+ </div>
283
+ <div>
284
+ <p className="text-sm font-medium text-muted-foreground">
285
+ {t('totalFaqs')}
286
+ </p>
287
+ <p className="text-2xl font-bold">{statsData?.total}</p>
288
+ </div>
289
+ </div>
290
+ </CardContent>
291
+ </Card>
292
+ </div>
293
+
294
+ <div className="mb-4 flex flex-col gap-4 md:flex-row mt-4">
295
+ <SearchBar
296
+ searchQuery={searchTerm}
297
+ onSearchChange={handleSearchChange}
298
+ onSearch={() => refetchFaq()}
299
+ placeholder={t('searchPlaceholder')}
300
+ />
301
+ </div>
302
+
303
+ <div className="space-y-4">
304
+ {faqs.length > 0 ? (
305
+ <div className="space-y-4">
306
+ {faqs.map((faq) => (
307
+ <Card
308
+ key={faq.id}
309
+ onDoubleClick={() => handleEditFAQ(faq)}
310
+ className="cursor-pointer transition-all duration-200 hover:border-primary/20 hover:shadow-md"
311
+ >
312
+ <CardContent className="p-6">
313
+ <div className="flex items-start justify-between gap-4">
314
+ <div className="flex-1 space-y-3">
315
+ <div className="flex items-start space-x-3">
316
+ <div className="mt-1 rounded-full bg-primary/10 p-2">
317
+ <HelpCircle className="h-4 w-4 text-primary" />
318
+ </div>
319
+ <div className="flex-1 space-y-2">
320
+ <div className="flex items-center gap-2 flex-wrap">
321
+ <h3 className="text-lg font-semibold leading-tight">
322
+ {faq.question}
323
+ </h3>
324
+ {faq.available_locales &&
325
+ faq.available_locales.length > 0 && (
326
+ <div className="flex gap-1 flex-wrap">
327
+ {faq.available_locales.map(
328
+ (locale: Locale) => (
329
+ <Badge
330
+ key={locale.code}
331
+ variant="outline"
332
+ className="text-xs"
333
+ >
334
+ <Globe className="mr-1 h-3 w-3" />
335
+ {locale.code.toUpperCase()}
336
+ </Badge>
337
+ )
338
+ )}
339
+ </div>
340
+ )}
341
+ </div>
342
+ <p className="line-clamp-2 text-sm text-muted-foreground">
343
+ {faq.answer}
344
+ </p>
345
+ </div>
346
+ </div>
347
+ </div>
348
+
349
+ <div className="flex flex-col gap-2">
350
+ <Button
351
+ variant="outline"
352
+ size="sm"
353
+ onClick={() => handleEditFAQ(faq)}
354
+ className="transition-colors hover:border-blue-200 hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-950"
355
+ >
356
+ <Edit className="mr-1 h-4 w-4" />
357
+ {t('edit')}
358
+ </Button>
359
+
360
+ <AlertDialog>
361
+ <AlertDialogTrigger asChild>
362
+ <Button
363
+ variant="outline"
364
+ size="sm"
365
+ className="bg-transparent transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950"
366
+ >
367
+ <Trash2 className="mr-1 h-4 w-4" />
368
+ {t('delete')}
369
+ </Button>
370
+ </AlertDialogTrigger>
371
+ <AlertDialogContent>
372
+ <AlertDialogHeader>
373
+ <AlertDialogTitle>
374
+ {t('confirmDelete')}
375
+ </AlertDialogTitle>
376
+ <AlertDialogDescription>
377
+ {t('deleteDescription')}
378
+ </AlertDialogDescription>
379
+ </AlertDialogHeader>
380
+ <AlertDialogFooter>
381
+ <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
382
+ <AlertDialogAction
383
+ onClick={() =>
384
+ handleDeleteFAQ(Number(faq.faq_id))
385
+ }
386
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
387
+ >
388
+ {t('delete')}
389
+ </AlertDialogAction>
390
+ </AlertDialogFooter>
391
+ </AlertDialogContent>
392
+ </AlertDialog>
393
+ </div>
394
+ </div>
395
+ </CardContent>
396
+ </Card>
397
+ ))}
398
+ </div>
399
+ ) : (
400
+ <Card>
401
+ <CardContent className="p-12 text-center">
402
+ <div className="flex flex-col items-center space-y-4">
403
+ <HelpCircle className="h-12 w-12 text-muted-foreground" />
404
+ <div>
405
+ <h3 className="text-lg font-semibold">
406
+ {t('noQuestionsFound')}
407
+ </h3>
408
+ <p className="text-muted-foreground">{t('adjustFilters')}</p>
409
+ </div>
410
+ <Button onClick={handleNewFAQ}>
411
+ <Plus className="mr-2 h-4 w-4" />
412
+ {t('createFirstQuestion')}
413
+ </Button>
414
+ </div>
415
+ </CardContent>
416
+ </Card>
417
+ )}
418
+
419
+ <PaginationFooter
420
+ currentPage={page}
421
+ pageSize={pageSize}
422
+ totalItems={total}
423
+ onPageChange={setPage}
424
+ onPageSizeChange={setPageSize}
425
+ pageSizeOptions={[10, 20, 30, 40, 50]}
426
+ />
427
+ </div>
428
+
429
+ <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
430
+ <DialogContent className="max-h-[95vh] max-w-4xl overflow-y-auto">
431
+ <DialogHeader>
432
+ <DialogTitle className="flex items-center space-x-2">
433
+ <Edit className="h-5 w-5" />
434
+ <span>
435
+ {isNewFAQ ? t('newQuestionTitle') : t('editQuestion')}
436
+ </span>
437
+ </DialogTitle>
438
+ <DialogDescription>
439
+ {isNewFAQ ? t('createDescription') : t('editDescription')}
440
+ </DialogDescription>
441
+ </DialogHeader>
442
+
443
+ {selectedFAQ && (
444
+ <div className="space-y-6">
445
+ {isEditDialogOpen && !isNewFAQ && (
446
+ <div className="space-y-2">
447
+ <Label
448
+ htmlFor="locale-select"
449
+ className="flex items-center gap-2"
450
+ >
451
+ <Globe className="h-4 w-4" />
452
+ {t('language')}
453
+ </Label>
454
+ <Select
455
+ value={selectedLocale}
456
+ onValueChange={setSelectedLocale}
457
+ >
458
+ <SelectTrigger id="locale-select">
459
+ <SelectValue placeholder={t('selectLanguage')} />
460
+ </SelectTrigger>
461
+ <SelectContent>
462
+ {locales.map((locale: Locale) => (
463
+ <SelectItem key={locale.code} value={locale.code}>
464
+ {locale.name}
465
+ </SelectItem>
466
+ ))}
467
+ </SelectContent>
468
+ </Select>
469
+ </div>
470
+ )}
471
+
472
+ {selectedLocale && selectedFAQ.locale?.[selectedLocale] && (
473
+ <div className="space-y-4">
474
+ <div className="space-y-2">
475
+ <Label htmlFor="question">{t('question')}</Label>
476
+ <Input
477
+ id="question"
478
+ placeholder={t('questionPlaceholder')}
479
+ value={selectedFAQ.locale[selectedLocale].question || ''}
480
+ onChange={(e) =>
481
+ setSelectedFAQ({
482
+ ...selectedFAQ,
483
+ locale: {
484
+ ...selectedFAQ.locale,
485
+ [selectedLocale]: {
486
+ ...selectedFAQ.locale[selectedLocale],
487
+ question: e.target.value,
488
+ },
489
+ },
490
+ })
491
+ }
492
+ />
493
+ </div>
494
+
495
+ <div className="space-y-2">
496
+ <Label htmlFor="answer">{t('answer')}</Label>
497
+ <Textarea
498
+ id="answer"
499
+ placeholder={t('answerPlaceholder')}
500
+ value={selectedFAQ.locale[selectedLocale].answer || ''}
501
+ onChange={(e) =>
502
+ setSelectedFAQ({
503
+ ...selectedFAQ,
504
+ locale: {
505
+ ...selectedFAQ.locale,
506
+ [selectedLocale]: {
507
+ ...selectedFAQ.locale[selectedLocale],
508
+ answer: e.target.value,
509
+ },
510
+ },
511
+ })
512
+ }
513
+ rows={6}
514
+ className="resize-none"
515
+ />
516
+ </div>
517
+ </div>
518
+ )}
519
+ </div>
520
+ )}
521
+
522
+ <DialogFooter className="mt-4">
523
+ <Button
524
+ variant="outline"
525
+ onClick={() => {
526
+ setIsEditDialogOpen(false);
527
+ setSelectedFAQ(null);
528
+ setIsNewFAQ(false);
529
+ }}
530
+ >
531
+ <X className="mr-2 h-4 w-4" />
532
+ {t('cancel')}
533
+ </Button>
534
+ <Button
535
+ onClick={handleSaveFAQ}
536
+ disabled={
537
+ !selectedFAQ?.locale ||
538
+ !Object.values(selectedFAQ.locale).some(
539
+ (l: any) => l.question && l.answer
540
+ )
541
+ }
542
+ className="transition-colors hover:bg-primary/90"
543
+ >
544
+ <Save className="mr-2 h-4 w-4" />
545
+ {isNewFAQ ? t('createQuestionButton') : t('saveChanges')}
546
+ </Button>
547
+ </DialogFooter>
548
+ </DialogContent>
549
+ </Dialog>
550
+ </div>
551
+ );
552
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "Faq": {
3
+ "title": "Frequently Asked Questions",
4
+ "description": "Manage all system FAQs",
5
+ "newQuestion": "New Question",
6
+ "totalFaqs": "Total FAQs",
7
+ "searchPlaceholder": "Search for question or answer...",
8
+ "noQuestionsFound": "No questions found",
9
+ "adjustFilters": "Try adjusting the filters or create a new question.",
10
+ "createFirstQuestion": "Create First Question",
11
+ "edit": "Edit",
12
+ "delete": "Delete",
13
+ "confirmDelete": "Confirm Deletion",
14
+ "deleteDescription": "Are you sure you want to delete this FAQ? This action cannot be undone.",
15
+ "cancel": "Cancel",
16
+ "editQuestion": "Edit FAQ",
17
+ "newQuestionTitle": "New FAQ",
18
+ "editDescription": "Edit the FAQ information",
19
+ "createDescription": "Create a new FAQ",
20
+ "language": "Language",
21
+ "selectLanguage": "Select a language",
22
+ "question": "Question",
23
+ "questionPlaceholder": "Enter the question",
24
+ "answer": "Answer",
25
+ "answerPlaceholder": "Enter the detailed answer",
26
+ "createQuestionButton": "Create Question",
27
+ "saveChanges": "Save Changes",
28
+ "errorLoading": "Error loading FAQ data.",
29
+ "successUpdate": "FAQ updated successfully!",
30
+ "successCreate": "FAQ created successfully!",
31
+ "errorSave": "Error saving FAQ.",
32
+ "successDelete": "FAQ deleted successfully!",
33
+ "errorDelete": "Error deleting FAQ."
34
+ }
35
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "Faq": {
3
+ "title": "Perguntas Frequentes",
4
+ "description": "Gerencie todas as perguntas frequentes do sistema",
5
+ "newQuestion": "Nova Pergunta",
6
+ "totalFaqs": "Total de FAQs",
7
+ "searchPlaceholder": "Buscar por pergunta ou resposta...",
8
+ "noQuestionsFound": "Nenhuma pergunta encontrada",
9
+ "adjustFilters": "Tente ajustar os filtros ou criar uma nova pergunta.",
10
+ "createFirstQuestion": "Criar Primeira Pergunta",
11
+ "edit": "Editar",
12
+ "delete": "Excluir",
13
+ "confirmDelete": "Confirmar Exclusão",
14
+ "deleteDescription": "Tem certeza que deseja excluir esta pergunta frequente? Esta ação não pode ser desfeita.",
15
+ "cancel": "Cancelar",
16
+ "editQuestion": "Editar Pergunta Frequente",
17
+ "newQuestionTitle": "Nova Pergunta Frequente",
18
+ "editDescription": "Edite as informações da pergunta frequente",
19
+ "createDescription": "Crie uma nova pergunta frequente",
20
+ "language": "Idioma",
21
+ "selectLanguage": "Selecione um idioma",
22
+ "question": "Pergunta",
23
+ "questionPlaceholder": "Digite a pergunta",
24
+ "answer": "Resposta",
25
+ "answerPlaceholder": "Digite a resposta detalhada",
26
+ "createQuestionButton": "Criar Pergunta",
27
+ "saveChanges": "Salvar Alterações",
28
+ "errorLoading": "Erro ao carregar os dados do FAQ.",
29
+ "successUpdate": "FAQ atualizado com sucesso!",
30
+ "successCreate": "FAQ criado com sucesso!",
31
+ "errorSave": "Erro ao salvar o FAQ.",
32
+ "successDelete": "FAQ excluído com sucesso!",
33
+ "errorDelete": "Erro ao excluir o FAQ."
34
+ }
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/faq",
3
- "version": "0.0.186",
3
+ "version": "0.0.190",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -9,9 +9,9 @@
9
9
  "@nestjs/core": "^11",
10
10
  "@nestjs/jwt": "^11",
11
11
  "@nestjs/mapped-types": "*",
12
- "@hed-hog/core": "0.0.186",
13
- "@hed-hog/api": "0.0.3",
12
+ "@hed-hog/core": "0.0.190",
14
13
  "@hed-hog/api-locale": "0.0.11",
14
+ "@hed-hog/api": "0.0.3",
15
15
  "@hed-hog/api-pagination": "0.0.5",
16
16
  "@hed-hog/api-prisma": "0.0.4"
17
17
  },