@hed-hog/contact-us 0.0.279 → 0.0.286

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,3 +1,4 @@
1
+ ```markdown
1
2
  # @hed-hog/contact-us
2
3
 
3
4
  ## 1. Visão geral do módulo
@@ -417,3 +418,4 @@ Resposta: objeto da mensagem excluída.
417
418
  ---
418
419
 
419
420
  Este módulo integra-se com os módulos `@hed-hog/api-locale`, `@hed-hog/api-prisma`, `@hed-hog/api-pagination` e `@hed-hog/core` para funcionalidades de localização, acesso ao banco de dados, paginação e envio de e-mails.
421
+ ```
@@ -1,6 +1,13 @@
1
1
  'use client';
2
2
 
3
- import { PageHeader, SearchBar } from '@/components/entity-list';
3
+ import {
4
+ EmptyState,
5
+ Page,
6
+ PageHeader,
7
+ PaginationFooter,
8
+ SearchBar,
9
+ StatsCards,
10
+ } from '@/components/entity-list';
4
11
  import {
5
12
  AlertDialog,
6
13
  AlertDialogAction,
@@ -31,6 +38,14 @@ import {
31
38
  SelectTrigger,
32
39
  SelectValue,
33
40
  } from '@/components/ui/select';
41
+ import {
42
+ Sheet,
43
+ SheetContent,
44
+ SheetDescription,
45
+ SheetFooter,
46
+ SheetHeader,
47
+ SheetTitle,
48
+ } from '@/components/ui/sheet';
34
49
  import { Textarea } from '@/components/ui/textarea';
35
50
  import { useDebounce } from '@/hooks/use-debounce';
36
51
  import { formatDateTime } from '@/lib/format-date';
@@ -94,7 +109,7 @@ interface ContactUs {
94
109
  user_contact_us_user_idTouser?: User;
95
110
  }
96
111
 
97
- export default function Page() {
112
+ export default function ContactUsPage() {
98
113
  const t = useTranslations('contact-us.ContactUs');
99
114
 
100
115
  const statusOptions = [
@@ -113,6 +128,8 @@ export default function Page() {
113
128
  const [searchTerm, setSearchTerm] = useState('');
114
129
  const debouncedSearch = useDebounce(searchTerm);
115
130
  const [statusFilter, setStatusFilter] = useState('all');
131
+ const [page, setPage] = useState(1);
132
+ const [pageSize, setPageSize] = useState(10);
116
133
  const [loadingRespond, setLoadingRespond] = useState(false);
117
134
 
118
135
  const { request, currentLocaleCode, getSettingValue } = useApp();
@@ -127,30 +144,32 @@ export default function Page() {
127
144
  },
128
145
  });
129
146
 
130
- const { data: contacts, refetch: refetchList } = useQuery<
131
- PaginationResult<ContactUs>
132
- >({
133
- queryKey: ['contact-us'],
147
+ const { data: contacts, refetch: refetchList } = useQuery<
148
+ PaginationResult<ContactUs>
149
+ >({
150
+ queryKey: ['contact-us', debouncedSearch, statusFilter, page, pageSize],
134
151
  queryFn: async () => {
135
152
  const response = await request({
136
153
  url: '/contact-us',
137
154
  params: {
155
+ page,
156
+ pageSize,
138
157
  search: debouncedSearch,
139
- status: statusFilter,
158
+ status: statusFilter === 'all' ? undefined : statusFilter,
140
159
  },
141
160
  });
142
161
  return response.data as PaginationResult<ContactUs>;
143
162
  },
144
- });
145
- const contactList = contacts ?? {
146
- data: [],
147
- total: 0,
148
- page: 1,
149
- pageSize: 10,
150
- lastPage: 1,
151
- prev: null,
152
- next: null,
153
- };
163
+ });
164
+ const contactList = contacts ?? {
165
+ data: [],
166
+ total: 0,
167
+ page: 1,
168
+ pageSize: 10,
169
+ lastPage: 1,
170
+ prev: null,
171
+ next: null,
172
+ };
154
173
 
155
174
  const getStatusBadge = (status: ContactUsStatusEnum) => {
156
175
  const statusOption = statusOptions.find((s) => s.value === status);
@@ -219,105 +238,89 @@ export default function Page() {
219
238
 
220
239
  useEffect(() => {
221
240
  refetchList();
222
- }, [debouncedSearch, statusFilter]);
241
+ }, [debouncedSearch, statusFilter, page, pageSize]);
242
+
243
+ const statsCards = [
244
+ {
245
+ title: t('totalContacts'),
246
+ value: stats?.total ?? 0,
247
+ icon: <MessageSquare className="h-5 w-5" />,
248
+ iconBgColor: 'bg-blue-50',
249
+ iconColor: 'text-blue-600',
250
+ },
251
+ {
252
+ title: t('new'),
253
+ value: stats?.totalNew ?? 0,
254
+ icon: <Clock className="h-5 w-5" />,
255
+ iconBgColor: 'bg-orange-50',
256
+ iconColor: 'text-orange-600',
257
+ },
258
+ {
259
+ title: t('responded'),
260
+ value: stats?.totalResponded ?? 0,
261
+ icon: <Reply className="h-5 w-5" />,
262
+ iconBgColor: 'bg-green-50',
263
+ iconColor: 'text-green-600',
264
+ },
265
+ {
266
+ title: t('inProgress'),
267
+ value: stats?.totalProgress ?? 0,
268
+ icon: <UserIcon className="h-5 w-5" />,
269
+ iconBgColor: 'bg-purple-50',
270
+ iconColor: 'text-purple-600',
271
+ },
272
+ ];
273
+
274
+ const searchControls = [
275
+ {
276
+ id: 'status',
277
+ type: 'select' as const,
278
+ value: statusFilter,
279
+ onChange: (value: string) => {
280
+ setStatusFilter(value);
281
+ setPage(1);
282
+ },
283
+ placeholder: t('status'),
284
+ options: [
285
+ { value: 'all', label: t('allStatus') },
286
+ ...statusOptions.map((status) => ({
287
+ value: status.value,
288
+ label: status.label,
289
+ })),
290
+ ],
291
+ },
292
+ ];
223
293
 
224
294
  return (
225
- <div className="flex flex-col h-screen px-4">
295
+ <Page>
226
296
  <PageHeader
227
- breadcrumbs={[
228
- { label: 'Home', href: '/' },
229
- { label: t('description') },
230
- ]}
297
+ breadcrumbs={[{ label: 'Home', href: '/' }, { label: t('title') }]}
231
298
  title={t('title')}
232
299
  description={t('description')}
233
300
  />
234
301
 
235
- <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
236
- <Card className="transition-shadow hover:shadow-md p-0">
237
- <CardContent className="p-4">
238
- <div className="flex items-center space-x-3">
239
- <MessageSquare className="h-10 w-10 text-blue-500" />
240
- <div>
241
- <p className="text-sm font-medium text-muted-foreground">
242
- {t('totalContacts')}
243
- </p>
244
- <p className="text-2xl font-bold">{stats?.total}</p>
245
- </div>
246
- </div>
247
- </CardContent>
248
- </Card>
249
-
250
- <Card className="transition-shadow hover:shadow-md p-0">
251
- <CardContent className="p-4">
252
- <div className="flex items-center space-x-3">
253
- <Clock className="h-10 w-10 text-orange-500" />
254
- <div>
255
- <p className="text-sm font-medium text-muted-foreground">
256
- {t('new')}
257
- </p>
258
- <p className="text-2xl font-bold">{stats?.totalNew}</p>
259
- </div>
260
- </div>
261
- </CardContent>
262
- </Card>
263
-
264
- <Card className="transition-shadow hover:shadow-md p-0">
265
- <CardContent className="p-4">
266
- <div className="flex items-center space-x-3">
267
- <Reply className="h-10 w-10 text-green-500" />
268
- <div>
269
- <p className="text-sm font-medium text-muted-foreground">
270
- {t('responded')}
271
- </p>
272
- <p className="text-2xl font-bold">{stats?.totalResponded}</p>
273
- </div>
274
- </div>
275
- </CardContent>
276
- </Card>
302
+ <StatsCards stats={statsCards} />
277
303
 
278
- <Card className="transition-shadow hover:shadow-md p-0">
279
- <CardContent className="p-4">
280
- <div className="flex items-center space-x-3">
281
- <UserIcon className="h-10 w-10 text-purple-500" />
282
- <div>
283
- <p className="text-sm font-medium text-muted-foreground">
284
- {t('inProgress')}
285
- </p>
286
- <p className="text-2xl font-bold">{stats?.totalProgress}</p>
287
- </div>
288
- </div>
289
- </CardContent>
290
- </Card>
291
- </div>
292
-
293
- <div className="mb-4 flex flex-col gap-4 md:flex-row mt-4">
304
+ <div className="mb-2 mt-2">
294
305
  <SearchBar
295
306
  searchQuery={searchTerm}
296
- onSearchChange={setSearchTerm}
297
- onSearch={() => refetchList()}
307
+ onSearchChange={(value) => {
308
+ setSearchTerm(value);
309
+ setPage(1);
310
+ }}
311
+ onSearch={() => {
312
+ setPage(1);
313
+ refetchList();
314
+ }}
298
315
  placeholder={t('searchPlaceholder')}
316
+ controls={searchControls}
299
317
  />
300
- <div className="flex gap-2 ">
301
- <Select value={statusFilter} onValueChange={setStatusFilter}>
302
- <SelectTrigger className="w-40">
303
- <SelectValue placeholder={t('status')} />
304
- </SelectTrigger>
305
- <SelectContent>
306
- <SelectItem value="all">{t('allStatus')}</SelectItem>
307
- {statusOptions.map((status) => (
308
- <SelectItem key={status.value} value={status.value}>
309
- {status.label}
310
- </SelectItem>
311
- ))}
312
- </SelectContent>
313
- </Select>
314
- </div>
315
318
  </div>
316
319
 
317
320
  <div className="space-y-4">
318
- {contactList.data.length > 0 ? (
319
- <div className="grid gap-4">
320
- {contactList.data.map((contact) => (
321
+ {contactList.data.length > 0 ? (
322
+ <div className="grid gap-4">
323
+ {contactList.data.map((contact) => (
321
324
  <Card
322
325
  key={contact.id}
323
326
  className="overflow-hidden transition-all duration-200 hover:border-primary/20 hover:shadow-md"
@@ -462,34 +465,30 @@ export default function Page() {
462
465
  ))}
463
466
  </div>
464
467
  ) : (
465
- <Card>
466
- <CardContent className="p-12 text-center">
467
- <div className="flex flex-col items-center space-y-4">
468
- <MessageSquare className="h-12 w-12 text-muted-foreground" />
469
- <div>
470
- <h3 className="text-lg font-semibold">
471
- {t('noContactsFound')}
472
- </h3>
473
- <p className="text-muted-foreground">
474
- {t('adjustFiltersSearch')}
475
- </p>
476
- </div>
477
- {(searchTerm || statusFilter !== 'all') && (
478
- <Button
479
- variant="outline"
480
- onClick={() => {
481
- setSearchTerm('');
482
- setStatusFilter('');
483
- refetchList();
484
- }}
485
- >
486
- {t('clearFilters')}
487
- </Button>
488
- )}
489
- </div>
490
- </CardContent>
491
- </Card>
468
+ <EmptyState
469
+ icon={<MessageSquare className="h-12 w-12" />}
470
+ title={t('noContactsFound')}
471
+ description={t('adjustFiltersSearch')}
472
+ actionLabel={t('clearFilters')}
473
+ onAction={() => {
474
+ setSearchTerm('');
475
+ setStatusFilter('all');
476
+ setPage(1);
477
+ refetchList();
478
+ }}
479
+ />
492
480
  )}
481
+
482
+ <PaginationFooter
483
+ currentPage={contactList.page || page}
484
+ pageSize={contactList.pageSize || pageSize}
485
+ totalItems={contactList.total}
486
+ onPageChange={setPage}
487
+ onPageSizeChange={(nextPageSize) => {
488
+ setPageSize(nextPageSize);
489
+ setPage(1);
490
+ }}
491
+ />
493
492
  </div>
494
493
 
495
494
  <Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
@@ -587,17 +586,25 @@ export default function Page() {
587
586
  </DialogContent>
588
587
  </Dialog>
589
588
 
590
- <Dialog open={isReplyDialogOpen} onOpenChange={setIsReplyDialogOpen}>
591
- <DialogContent className="max-w-2xl">
592
- <DialogHeader className="mb-2">
593
- <DialogTitle>{t('replyContact')}</DialogTitle>
594
- <DialogDescription>
589
+ <Sheet
590
+ open={isReplyDialogOpen}
591
+ onOpenChange={(open) => {
592
+ setIsReplyDialogOpen(open);
593
+ if (!open) {
594
+ setReplyMessage('');
595
+ }
596
+ }}
597
+ >
598
+ <SheetContent className="w-full overflow-y-auto sm:max-w-2xl">
599
+ <SheetHeader className="mb-2">
600
+ <SheetTitle>{t('replyContact')}</SheetTitle>
601
+ <SheetDescription>
595
602
  {t('sendResponseTo', { name: String(selectedContact?.name) })}
596
- </DialogDescription>
597
- </DialogHeader>
603
+ </SheetDescription>
604
+ </SheetHeader>
598
605
 
599
606
  {selectedContact && (
600
- <div className="space-y-4">
607
+ <div className="space-y-4 px-4">
601
608
  <div className="rounded-md border bg-muted/50 p-3">
602
609
  <div className="mb-2 text-sm font-medium">
603
610
  {t('originalMessage')}
@@ -622,21 +629,11 @@ export default function Page() {
622
629
  </div>
623
630
  )}
624
631
 
625
- <DialogFooter className="mt-2">
626
- <Button
627
- variant="outline"
628
- onClick={() => {
629
- setIsReplyDialogOpen(false);
630
- setReplyMessage('');
631
- }}
632
- >
633
- <X className="mr-2 h-4 w-4" />
634
- {t('cancel')}
635
- </Button>
632
+ <SheetFooter>
636
633
  <Button
637
634
  onClick={handleSendReply}
638
635
  disabled={!replyMessage.trim() || loadingRespond}
639
- className="transition-colors hover:bg-primary/90"
636
+ className="w-full transition-colors hover:bg-primary/90"
640
637
  >
641
638
  {loadingRespond ? (
642
639
  <>
@@ -650,9 +647,9 @@ export default function Page() {
650
647
  </>
651
648
  )}
652
649
  </Button>
653
- </DialogFooter>
654
- </DialogContent>
655
- </Dialog>
656
- </div>
650
+ </SheetFooter>
651
+ </SheetContent>
652
+ </Sheet>
653
+ </Page>
657
654
  );
658
655
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/contact-us",
3
- "version": "0.0.279",
3
+ "version": "0.0.286",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -9,11 +9,11 @@
9
9
  "@nestjs/core": "^11",
10
10
  "@nestjs/jwt": "^11",
11
11
  "@nestjs/mapped-types": "*",
12
- "@hed-hog/api-locale": "0.0.13",
12
+ "@hed-hog/core": "0.0.286",
13
+ "@hed-hog/api-pagination": "0.0.6",
13
14
  "@hed-hog/api-prisma": "0.0.5",
14
- "@hed-hog/core": "0.0.279",
15
- "@hed-hog/api": "0.0.4",
16
- "@hed-hog/api-pagination": "0.0.6"
15
+ "@hed-hog/api-locale": "0.0.13",
16
+ "@hed-hog/api": "0.0.4"
17
17
  },
18
18
  "exports": {
19
19
  ".": {