@hed-hog/faq 0.0.185 → 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.
|
|
3
|
+
"version": "0.0.190",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"@nestjs/core": "^11",
|
|
10
10
|
"@nestjs/jwt": "^11",
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
|
-
"@hed-hog/
|
|
13
|
-
"@hed-hog/core": "0.0.185",
|
|
14
|
-
"@hed-hog/api": "0.0.3",
|
|
12
|
+
"@hed-hog/core": "0.0.190",
|
|
15
13
|
"@hed-hog/api-locale": "0.0.11",
|
|
14
|
+
"@hed-hog/api": "0.0.3",
|
|
15
|
+
"@hed-hog/api-pagination": "0.0.5",
|
|
16
16
|
"@hed-hog/api-prisma": "0.0.4"
|
|
17
17
|
},
|
|
18
18
|
"exports": {
|