@c-rex/components 0.3.0-build.35 → 0.3.0-build.36

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.
Files changed (45) hide show
  1. package/package.json +28 -36
  2. package/src/article/article-action-bar.tsx +4 -1
  3. package/src/{check-article-lang.tsx → article/check-article-lang.tsx} +1 -1
  4. package/src/article/render-article-highlight.tsx +108 -0
  5. package/src/article/render-article.tsx +28 -0
  6. package/src/autocomplete.tsx +2 -2
  7. package/src/carousel/carousel.tsx +5 -2
  8. package/src/carousel/information-unit-carousel-item.tsx +1 -1
  9. package/src/content-unavailable.tsx +20 -0
  10. package/src/directoryNodes/directory-tree-context.tsx +9 -4
  11. package/src/documents/description-preview.tsx +14 -4
  12. package/src/documents/result-list-item.tsx +35 -46
  13. package/src/favorites/__tests__/favorites-hydration.test.tsx +245 -0
  14. package/src/favorites/bookmark-button.tsx +38 -20
  15. package/src/favorites/favorite-button.tsx +23 -24
  16. package/src/favorites/favorites-context.tsx +287 -0
  17. package/src/icons/file-icon.tsx +9 -26
  18. package/src/info/information-unit-metadata-grid-client.tsx +21 -21
  19. package/src/page-wrapper.tsx +1 -1
  20. package/src/renditions/html-client.tsx +8 -6
  21. package/src/renditions/html.tsx +3 -1
  22. package/src/restriction-menu/restriction-menu-item.tsx +48 -58
  23. package/src/restriction-menu/restriction-selection-command-menu.tsx +444 -0
  24. package/src/restriction-menu/restriction-selection-menu.tsx +3 -5
  25. package/src/restriction-menu/taxonomy-restriction-command-menu.tsx +111 -0
  26. package/src/restriction-menu/taxonomy-restriction-menu.tsx +1 -7
  27. package/src/results/filter-navbar.tsx +81 -76
  28. package/src/results/filter-sidebar/context.tsx +32 -0
  29. package/src/results/filter-sidebar/index.tsx +44 -35
  30. package/src/results/generic/search-results-client.tsx +5 -4
  31. package/src/results/generic/table-result-list.tsx +16 -16
  32. package/src/results/information-unit-search-results-card-list.tsx +4 -1
  33. package/src/results/pagination.tsx +43 -40
  34. package/src/search-input.tsx +4 -2
  35. package/src/toc/toc-browse-controls.tsx +2 -2
  36. package/src/toc/toc-tree-panel.tsx +19 -16
  37. package/src/article/article-content.tsx +0 -19
  38. package/src/breadcrumb.tsx +0 -124
  39. package/src/directoryNodes/tree-of-content.tsx +0 -68
  40. package/src/render-article.tsx +0 -75
  41. package/src/restriction-menu/restriction-menu-container.tsx +0 -4
  42. package/src/restriction-menu/restriction-menu.tsx +0 -4
  43. package/src/stores/__tests__/favorites-store.test.ts +0 -54
  44. package/src/stores/favorites-store.ts +0 -163
  45. /package/src/{render-article.module.css → article/render-article.module.css} +0 -0
@@ -6,9 +6,11 @@ import { Button } from "@c-rex/ui/button";
6
6
  import { Funnel, X } from "lucide-react";
7
7
  import { Badge } from "@c-rex/ui/badge";
8
8
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@c-rex/ui/tooltip";
9
- import { OPERATOR_OPTIONS } from "@c-rex/constants";
9
+ import { DEVICE_OPTIONS, OPERATOR_OPTIONS } from "@c-rex/constants";
10
10
  import { Tags } from "@c-rex/interfaces";
11
+ import { useBreakpoint } from "@c-rex/ui/hooks";
11
12
  import { applyFacetPropertyVisibility, FacetLabelOverrides, memoizeFilteredTags } from "./filter-sidebar/utils";
13
+ import { useFilterSidebarState } from "./filter-sidebar/context";
12
14
  import { useSearchSettingsStore } from "../stores/search-settings-store";
13
15
  import { useRestrictionStore } from "../stores/restriction-store";
14
16
  import { useSearchNavigationStore } from "../stores/search-navigation-store";
@@ -38,6 +40,9 @@ export const FilterNavbar: FC<FilterNavbarProps> = ({
38
40
  }) => {
39
41
  const t = useTranslations()
40
42
  const locale = useLocale()
43
+ const device = useBreakpoint();
44
+ const isMobile = device !== null && device === DEVICE_OPTIONS.MOBILE
45
+ const { setIsMobileFiltersOpen } = useFilterSidebarState();
41
46
  const restrictionList = useRestrictionStore((state) => state.restrictionList);
42
47
  const startSearchNavigation = useSearchNavigationStore((state) => state.start);
43
48
  const [params, setParams] = useQueryStates({
@@ -59,8 +64,6 @@ export const FilterNavbar: FC<FilterNavbarProps> = ({
59
64
  history: 'push',
60
65
  shallow: false,
61
66
  });
62
- const isMobile = false
63
-
64
67
 
65
68
  const filteredTags = useMemo(() => {
66
69
  const resolved = memoizeFilteredTags(tags, params.filter, params.packages, {
@@ -70,6 +73,7 @@ export const FilterNavbar: FC<FilterNavbarProps> = ({
70
73
  });
71
74
  return applyFacetPropertyVisibility(resolved, includeProperties, excludeProperties);
72
75
  }, [tags, params.filter, params.packages, locale, facetLabelOverrides, includeZeroHits, includeProperties, excludeProperties]);
76
+ const hasMobileFilters = Object.entries(filteredTags).length > 0;
73
77
 
74
78
  const humanizeMetadataKey = (key: string): string =>
75
79
  key
@@ -193,99 +197,100 @@ export const FilterNavbar: FC<FilterNavbarProps> = ({
193
197
  })
194
198
 
195
199
  return filters
196
- }, [filteredTags, params]);
200
+ }, [filteredTags, params, resolveSectionLabelByKey, restrictionList, t]);
197
201
 
198
202
  if (filters == null || filters.length === 0) {
199
203
  return null;
200
204
  }
201
205
 
202
206
  return (
203
- <div className="pb-4 flex justify-between">
204
-
205
- <div className="flex flex-wrap gap-2">
207
+ <div className="pb-4 flex flex-wrap gap-1">
208
+ {hasMobileFilters && (
206
209
  <Button
207
210
  size="sm"
208
211
  variant="secondary"
209
- //onClick={() => setOpen(true)}
210
- className="md:hidden"
212
+ className="lg:hidden"
213
+ aria-label={t("filter.filters")}
214
+ onClick={() => setIsMobileFiltersOpen(true)}
211
215
  >
212
216
  <Funnel className="h-2" />
213
217
  </Button>
214
-
215
- {filters.length > 0 && (
216
- <>
217
- {filters.slice(0, 1).map((item) => (
218
- <Badge
219
- key={`${item.key}-${item?.value}`}
220
- variant="outline"
221
- className="h-8"
222
- >
223
- {item?.name ? item.name : item.key}: {item.value}
224
-
225
- {item.removable && (
226
- <Button size="xs" variant="ghost" onClick={() => {
227
- startSearchNavigation();
228
- setParams({ [item.key]: item?.default })
229
- }}>
230
- <X className="h-2" />
231
- </Button>
232
- )}
233
- </Badge>
234
- ))}
235
-
236
-
237
- {isMobile ? (
238
- <TooltipProvider>
239
- <Tooltip delayDuration={100}>
240
- <TooltipTrigger>
241
- <Badge
242
- key={`grouped-filters`}
243
- variant="outline"
244
- className="h-8"
245
- >
246
- +{filters.length - 1} {t("filter.filters")}
247
- </Badge>
248
-
249
- </TooltipTrigger>
250
-
251
- <TooltipContent>
252
- {filters.slice(1).map((item) => {
253
- const label = item?.name ? item.name : item.key
254
- const returnString = `${label}: ${item.value}`;
255
- return <div className="capitalize" key={returnString}>{returnString}</div>;
256
- })}
257
- </TooltipContent>
258
- </Tooltip>
259
- </TooltipProvider>
260
- ) : (
261
- <>
262
- {filters.slice(1).map((item) => (
218
+ )}
219
+
220
+ {filters.length > 0 && (
221
+ <>
222
+ {filters.slice(0, 1).map((item) => (
223
+ <Badge
224
+ key={`${item.key}-${item?.value}`}
225
+ variant="outline"
226
+ className="h-8"
227
+ >
228
+ {item?.name ? item.name : item.key}: {item.value}
229
+
230
+ {item.removable && (
231
+ <Button size="xs" variant="ghost" onClick={() => {
232
+ startSearchNavigation();
233
+ setParams({ [item.key]: item?.default })
234
+ }}>
235
+ <X className="h-2" />
236
+ </Button>
237
+ )}
238
+ </Badge>
239
+ ))}
240
+
241
+
242
+ {isMobile && filters.length > 1 ? (
243
+ <TooltipProvider>
244
+ <Tooltip delayDuration={100}>
245
+ <TooltipTrigger>
263
246
  <Badge
264
- key={`${item.key}-${item?.value}`}
247
+ key={`grouped-filters`}
265
248
  variant="outline"
266
249
  className="h-8"
267
250
  >
268
- {item?.name ? item.name : item.key}: {item.value}
269
-
270
- {item.removable && (
271
- <Button size="xs" variant="ghost" onClick={() => {
272
- startSearchNavigation();
273
- setParams({ [item.key]: item?.default })
274
- }}>
275
- <X className="h-2" />
276
- </Button>
277
- )}
251
+ +{filters.length - 1} {t("filter.filters")}
278
252
  </Badge>
279
- ))}
280
- </>
281
- )}
282
- </>
283
- )}
284
- </div>
253
+
254
+ </TooltipTrigger>
255
+
256
+ <TooltipContent>
257
+ {filters.slice(1).map((item) => {
258
+ const label = item?.name ? item.name : item.key
259
+ const returnString = `${label}: ${item.value}`;
260
+ return <div className="capitalize" key={returnString}>{returnString}</div>;
261
+ })}
262
+ </TooltipContent>
263
+ </Tooltip>
264
+ </TooltipProvider>
265
+ ) : (
266
+ <>
267
+ {filters.slice(1).map((item) => (
268
+ <Badge
269
+ key={`${item.key}-${item?.value}`}
270
+ variant="outline"
271
+ className="h-8"
272
+ >
273
+ {item?.name ? item.name : item.key}: {item.value}
274
+
275
+ {item.removable && (
276
+ <Button size="xs" variant="ghost" onClick={() => {
277
+ startSearchNavigation();
278
+ setParams({ [item.key]: item?.default })
279
+ }}>
280
+ <X className="h-2" />
281
+ </Button>
282
+ )}
283
+ </Badge>
284
+ ))}
285
+ </>
286
+ )}
287
+ </>
288
+ )}
285
289
 
286
290
  <Button
287
291
  size="sm"
288
292
  variant="secondary"
293
+ className="ml-auto"
289
294
  onClick={() => {
290
295
  startSearchNavigation();
291
296
  setParams({ filter: null, packages: null })
@@ -294,6 +299,6 @@ export const FilterNavbar: FC<FilterNavbarProps> = ({
294
299
  {t("reset")}
295
300
  </Button>
296
301
 
297
- </div>
302
+ </div >
298
303
  );
299
304
  };
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { FC, ReactNode, createContext, useContext, useMemo, useState } from "react";
4
+
5
+ type FilterSidebarContextValue = {
6
+ isMobileFiltersOpen: boolean
7
+ setIsMobileFiltersOpen: (open: boolean) => void
8
+ };
9
+
10
+ const noop = () => undefined;
11
+
12
+ const FilterSidebarContext = createContext<FilterSidebarContextValue>({
13
+ isMobileFiltersOpen: false,
14
+ setIsMobileFiltersOpen: noop,
15
+ });
16
+
17
+ export const FilterSidebarProvider: FC<{ children: ReactNode }> = ({ children }) => {
18
+ const [isMobileFiltersOpen, setIsMobileFiltersOpen] = useState(false);
19
+
20
+ const value = useMemo(() => ({
21
+ isMobileFiltersOpen,
22
+ setIsMobileFiltersOpen,
23
+ }), [isMobileFiltersOpen]);
24
+
25
+ return (
26
+ <FilterSidebarContext.Provider value={value}>
27
+ {children}
28
+ </FilterSidebarContext.Provider>
29
+ );
30
+ };
31
+
32
+ export const useFilterSidebarState = () => useContext(FilterSidebarContext);
@@ -1,9 +1,11 @@
1
1
  "use client";
2
2
 
3
- import { FC, useMemo, useState } from "react";
3
+ import { FC, useEffect, useMemo, useState } from "react";
4
4
  import { useLocale, useTranslations } from 'next-intl'
5
5
  import { Check, ChevronDown, ChevronRight } from "lucide-react"
6
6
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@c-rex/ui/collapsible";
7
+ import { useBreakpoint } from "@c-rex/ui/hooks";
8
+ import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@c-rex/ui/sheet";
7
9
  import {
8
10
  SidebarContent,
9
11
  SidebarGroup,
@@ -30,6 +32,8 @@ import {
30
32
  import { FilterItem, Tags } from "@c-rex/interfaces";
31
33
  import type { TaxonomyResult } from "@c-rex/services/read-models";
32
34
  import { useSearchNavigationStore } from "../../stores/search-navigation-store";
35
+ import { DEVICE_OPTIONS } from "@c-rex/constants";
36
+ import { useFilterSidebarState } from "./context";
33
37
 
34
38
 
35
39
  export interface FilterSidebarProps {
@@ -42,9 +46,7 @@ export interface FilterSidebarProps {
42
46
  excludeProperties?: string[]
43
47
  }
44
48
 
45
-
46
- //TODO; check layout on mobile
47
- export const FilterSidebar: FC<FilterSidebarProps> = ({
49
+ const FilterSidebar: FC<FilterSidebarProps> = ({
48
50
  tags,
49
51
  totalItemCount,
50
52
  facetLabelOverrides,
@@ -55,6 +57,9 @@ export const FilterSidebar: FC<FilterSidebarProps> = ({
55
57
  }) => {
56
58
  const t = useTranslations();
57
59
  const locale = useLocale();
60
+ const device = useBreakpoint();
61
+ const isMobile = device !== null && (device === DEVICE_OPTIONS.MOBILE || device === DEVICE_OPTIONS.TABLET);
62
+ const { isMobileFiltersOpen, setIsMobileFiltersOpen } = useFilterSidebarState();
58
63
  const [params, setParams] = useQueryStates({
59
64
  packages: parseAsString,
60
65
  filter: parseAsString,
@@ -64,6 +69,10 @@ export const FilterSidebar: FC<FilterSidebarProps> = ({
64
69
  });
65
70
  const startSearchNavigation = useSearchNavigationStore((state) => state.start);
66
71
 
72
+ useEffect(() => {
73
+ console.log(isMobile, isMobileFiltersOpen)
74
+ }, [isMobile, isMobileFiltersOpen]);
75
+
67
76
  const filteredTags = useMemo(() => {
68
77
  const resolved = memoizeFilteredTags(tags, params.filter, params.packages, {
69
78
  uiLanguage: locale,
@@ -379,8 +388,7 @@ export const FilterSidebar: FC<FilterSidebarProps> = ({
379
388
  };
380
389
 
381
390
  const content = (
382
- <SidebarContent className="!gap-0" suppressHydrationWarning>
383
-
391
+ <SidebarContent className="!gap-0 capitalize" suppressHydrationWarning>
384
392
  {Object.entries(filteredTags).map(([key, value]) => {
385
393
  const items = value as ResolvedFilterItem[];
386
394
  const hasTaxonomy = Boolean(facetTaxonomies?.[key]);
@@ -403,7 +411,7 @@ export const FilterSidebar: FC<FilterSidebarProps> = ({
403
411
  <Collapsible defaultOpen className="py-0 group/collapsible">
404
412
  <SidebarGroup className="!p-1">
405
413
  <SidebarGroupLabel asChild className="hover:bg-sidebar-accent text-sidebar-accent-foreground text-sm font-bold">
406
- <CollapsibleTrigger className="!h-8">
414
+ <CollapsibleTrigger className="!h-8 capitalize">
407
415
  {propertyLabel}
408
416
  <ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
409
417
  </CollapsibleTrigger>
@@ -517,36 +525,37 @@ export const FilterSidebar: FC<FilterSidebarProps> = ({
517
525
  </SidebarContent>
518
526
  )
519
527
 
520
- /*
521
- if (isMobile) {
522
- return (
523
-
524
- <SheetContent
525
- side="left"
526
- className="!pt-6 !px-2 w-[400px] overflow-y-auto"
527
- >
528
- <SheetHeader className="justify-center items-end font-bold">
529
- {t("filter.filters")}
530
- <span className="text-xs text-muted-foreground leading-5">
531
- {totalItemCount} {t("results.results")}
532
- </span>
533
- </SheetHeader>
534
- {content}
535
- </SheetContent>
536
- )
537
- }
538
- */
539
528
  if (Object.entries(filteredTags).length === 0) return null;
540
529
 
541
530
  return (
542
- <div className="w-60 lg:w-80 bg-sidebar rounded-md border pb-4">
543
- <SidebarHeader className="justify-center items-end font-bold">
544
- {t("filter.filters")}
545
- <span className="text-xs text-muted-foreground leading-5">
546
- {totalItemCount} {t("results.results")}
547
- </span>
548
- </SidebarHeader>
549
- {content}
550
- </div>
531
+ <>
532
+ <Sheet open={isMobile && isMobileFiltersOpen} onOpenChange={setIsMobileFiltersOpen}>
533
+ <SheetContent
534
+ side="left"
535
+ className="w-[calc(100vw-2rem)] max-w-sm overflow-y-auto !px-2 !pt-6 lg:hidden"
536
+ >
537
+ <SheetHeader className="justify-center items-end font-bold">
538
+ <SheetTitle>{t("filter.filters")}</SheetTitle>
539
+ <span className="text-xs text-muted-foreground leading-5">
540
+ {totalItemCount} {t("results.results")}
541
+ </span>
542
+ </SheetHeader>
543
+ {content}
544
+ </SheetContent>
545
+ </Sheet>
546
+
547
+ <div className="hidden w-60 rounded-md border bg-sidebar pb-4 lg:block lg:w-80">
548
+ <SidebarHeader className="justify-center items-end font-bold">
549
+ {t("filter.filters")}
550
+ <span className="text-xs text-muted-foreground leading-5">
551
+ {totalItemCount} {t("results.results")}
552
+ </span>
553
+ </SidebarHeader>
554
+ {content}
555
+ </div>
556
+ </>
551
557
  );
552
558
  };
559
+
560
+ export { FilterSidebarProvider } from "./context";
561
+ export { FilterSidebar };
@@ -156,9 +156,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
156
156
  />
157
157
 
158
158
  <div className="flex-1 flex flex-col gap-4">
159
- <Pagination pageInfo={data.pageInfo} className="pt-0" />
160
159
  <Empty />
161
- <Pagination pageInfo={data.pageInfo} />
162
160
  </div>
163
161
  </div>
164
162
  </div>
@@ -183,7 +181,10 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
183
181
  />
184
182
 
185
183
  <div className="flex-1 flex flex-col gap-4">
186
- <Pagination pageInfo={data.pageInfo} className="pt-0" />
184
+ {Number(data.pageInfo.pageCount) > 1 && (
185
+ <Pagination pageInfo={data.pageInfo} className="pt-0" />
186
+ )}
187
+
187
188
  <GenericTableResultList
188
189
  items={data.items}
189
190
  query={search}
@@ -217,7 +218,7 @@ export const GenericSearchResultsClient = <
217
218
  const t = useTranslations();
218
219
 
219
220
  return (
220
- <div className="container">
221
+ <div className="flex-1 container">
221
222
  <div className="flex gap-6 py-6">
222
223
  <div className="flex-1">
223
224
  <SuggestionsComponent
@@ -30,12 +30,12 @@ export const GenericTableResultList: FC<Props> = ({
30
30
 
31
31
  return (
32
32
  <div className="rounded-md border mb-6 last:border-b-0">
33
- <div className="font-bold text-sm p-2 border-b items-center flex-wrap hidden md:flex">
34
- <div className="flex-1 p-2">{t("title")}</div>
35
- <div className="w-1/2 md:w-1/5 p-2 flex justify-center">{t("language")}</div>
36
- <div className="w-1/2 md:w-1/5 p-2 flex justify-center">{t("type")}</div>
33
+ <div className="font-bold text-sm px-2 py-4 gap-2 border-b items-center flex">
34
+ <div className="flex-1">{t("title")}</div>
35
+ <div className="w-28 flex justify-center">{t("type")}</div>
36
+ <div className="w-20 flex justify-center">{t("language")}</div>
37
37
  {showFilesColumn &&
38
- <div className="w-1/2 md:w-1/5 p-2 flex justify-center">{t("files")}</div>
38
+ <div className="w-8 flex justify-center">{t("files")}</div>
39
39
  }
40
40
  </div>
41
41
 
@@ -63,31 +63,31 @@ export const GenericTableResultList: FC<Props> = ({
63
63
  return (
64
64
  <div
65
65
  className={cn(
66
- "min-h-12 c-rex-result-item flex flex-wrap items-center border-b",
66
+ "min-h-12 c-rex-result-item flex flex-wrap items-center border-b p-2 gap-2",
67
67
  `c-rex-result-${itemType.toLowerCase()}`,
68
68
  )}
69
69
  key={item.shortId}
70
70
  >
71
- <div className="flex-1 p-2">
71
+ <div className="flex-1">
72
72
  <Link href={link} className="hover:underline">{title}</Link>
73
73
  </div>
74
74
 
75
- <div className="w-1/5 md:w-1/5 flex justify-center p-2">
76
- <span className="w-8 inline-block">
75
+ <div className="w-28 flex justify-center">
76
+ <Badge variant="secondary">
77
+ {itemType}
78
+ </Badge>
79
+ </div>
80
+
81
+ <div className="w-20 flex justify-center">
82
+ <span className="w-8 inline-block">
77
83
  {countryCode.length > 0 && (
78
84
  <Flag countryCode={countryCode} />
79
85
  )}
80
86
  </span>
81
87
  </div>
82
88
 
83
- <div className="w-4/5 md:w-1/5 p-2 flex justify-center">
84
- <Badge>
85
- {itemType}
86
- </Badge>
87
- </div>
88
-
89
89
  {showFilesColumn &&
90
- <div className="w-1/5 flex justify-center p-2">
90
+ <div className="w-1/5 flex justify-center">
91
91
  <FileDownloadDropdown renditions={item.renditions} />
92
92
  </div>
93
93
  }
@@ -1,4 +1,5 @@
1
1
  import { FC } from "react";
2
+ import { useTranslations } from "next-intl";
2
3
  import { CommonItemsModel } from "@c-rex/interfaces";
3
4
  import { FileStack } from "lucide-react";
4
5
  import { FileDownloadDropdown } from "../renditions/file-download";
@@ -32,6 +33,8 @@ export const InformationUnitSearchResultsCardList: FC<InformationUnitSearchResul
32
33
  imageFragmentSubjectIds = [],
33
34
  descriptionFragmentSubjectIds = [],
34
35
  }) => {
36
+ const t = useTranslations();
37
+
35
38
  return (
36
39
  <div className="flex-1">
37
40
  {items.map((item, index) => {
@@ -115,7 +118,7 @@ export const InformationUnitSearchResultsCardList: FC<InformationUnitSearchResul
115
118
  </Button>
116
119
  </TooltipTrigger>
117
120
  <TooltipContent>
118
- Available in: {multipleVersions.join(", ")}
121
+ {t("availableIn")}: {multipleVersions.join(", ")}
119
122
  </TooltipContent>
120
123
  </Tooltip>
121
124
  )}
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import { FC, MouseEvent } from "react";
3
+ import { FC } from "react";
4
4
  import {
5
5
  Pagination as PaginationUI,
6
6
  PaginationContent,
@@ -10,7 +10,7 @@ import {
10
10
  PaginationNext,
11
11
  PaginationPrevious,
12
12
  } from "@c-rex/ui/pagination"
13
- import { parseAsInteger, useQueryState } from "nuqs";
13
+ import { usePathname, useSearchParams } from "next/navigation";
14
14
  import { ResultContainerPageInfoModel } from "@c-rex/interfaces";
15
15
  import { useTranslations } from "next-intl";
16
16
  import { cn } from "@c-rex/utils";
@@ -22,6 +22,30 @@ interface PaginationProps {
22
22
 
23
23
  type PaginationToken = number | "ellipsis-left" | "ellipsis-right";
24
24
 
25
+ const isValidPageNumber = (pageNumber: number, currentPage: number, pageCount: number) => (
26
+ pageNumber >= 1
27
+ && pageNumber <= pageCount
28
+ && pageNumber !== currentPage
29
+ );
30
+
31
+ export const buildPageHref = (
32
+ pathname: string,
33
+ currentSearch: string,
34
+ pageNumber: number,
35
+ currentPage: number,
36
+ pageCount: number,
37
+ ) => {
38
+ if (!isValidPageNumber(pageNumber, currentPage, pageCount)) {
39
+ return `${pathname}${currentSearch}`;
40
+ }
41
+
42
+ const searchParams = new URLSearchParams(currentSearch);
43
+ searchParams.set("page", String(pageNumber));
44
+
45
+ const queryString = searchParams.toString();
46
+ return queryString.length > 0 ? `${pathname}?${queryString}` : pathname;
47
+ };
48
+
25
49
  const buildPaginationTokens = (currentPage: number, pageCount: number): PaginationToken[] => {
26
50
  if (pageCount <= 7) {
27
51
  return Array.from({ length: pageCount }, (_, index) => index + 1);
@@ -41,27 +65,8 @@ const buildPaginationTokens = (currentPage: number, pageCount: number): Paginati
41
65
  export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
42
66
  const disabledClass = "opacity-50 pointer-events-none";
43
67
  const t = useTranslations("results");
44
-
45
- const [, setPage] = useQueryState('page',
46
- parseAsInteger.withOptions({
47
- history: 'push',
48
- shallow: false,
49
- })
50
- )
51
-
52
- const onChangePage = (event: MouseEvent<HTMLAnchorElement>, pageNumber: number) => {
53
- event.preventDefault();
54
-
55
- if (
56
- pageNumber < 1
57
- || (pageInfo.pageCount !== undefined && pageNumber > pageInfo.pageCount)
58
- || pageNumber === pageInfo.pageNumber
59
- ) {
60
- return;
61
- }
62
-
63
- void setPage(pageNumber);
64
- }
68
+ const pathname = usePathname();
69
+ const searchParams = useSearchParams();
65
70
 
66
71
  const pageNumber = pageInfo.pageNumber || 1;
67
72
  const pageCount = pageInfo.pageCount || 1;
@@ -69,32 +74,25 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
69
74
  const firstItemOnPage = pageInfo.firstItemOnPage || 0;
70
75
  const lastItemOnPage = pageInfo.lastItemOnPage || 0;
71
76
  const tokens = buildPaginationTokens(pageNumber, pageCount);
77
+ const currentSearch = searchParams.toString();
78
+ const search = currentSearch.length > 0 ? `?${currentSearch}` : "";
72
79
 
73
80
  return (
74
81
  <div className={cn("flex flex-col gap-3 py-4", className)}>
75
- <div className="flex flex-col gap-1 text-sm text-muted-foreground md:flex-row md:items-center md:justify-between">
76
- <span>
82
+ <PaginationUI className="py-4 items-center justify-center sm:justify-between">
83
+ <span className="hidden sm:block text-sm text-muted-foreground">
77
84
  {t("paginationResults", {
78
85
  first: firstItemOnPage,
79
86
  last: lastItemOnPage,
80
87
  total: totalItemCount,
81
88
  })}
82
89
  </span>
83
- <span>
84
- {t("paginationPage", {
85
- page: pageNumber,
86
- pageCount,
87
- })}
88
- </span>
89
- </div>
90
90
 
91
- <PaginationUI className="py-0">
92
- <PaginationContent className="justify-start">
91
+ <PaginationContent className="justify-between sm:justify-start w-full sm:w-auto">
93
92
  <PaginationItem>
94
93
  <PaginationPrevious
95
- href="#"
94
+ href={buildPageHref(pathname, search, pageNumber - 1, pageNumber, pageCount)}
96
95
  className={pageNumber === 1 ? disabledClass : ""}
97
- onClick={(event) => onChangePage(event, pageNumber - 1)}
98
96
  />
99
97
  </PaginationItem>
100
98
 
@@ -102,9 +100,8 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
102
100
  typeof token === "number" ? (
103
101
  <PaginationItem key={token}>
104
102
  <PaginationLink
105
- href="#"
103
+ href={buildPageHref(pathname, search, token, pageNumber, pageCount)}
106
104
  isActive={token === pageNumber}
107
- onClick={(event) => onChangePage(event, token)}
108
105
  >
109
106
  {token}
110
107
  </PaginationLink>
@@ -118,12 +115,18 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
118
115
 
119
116
  <PaginationItem>
120
117
  <PaginationNext
121
- href="#"
122
- onClick={(event) => onChangePage(event, pageNumber + 1)}
118
+ href={buildPageHref(pathname, search, pageNumber + 1, pageNumber, pageCount)}
123
119
  className={pageNumber === pageCount ? disabledClass : ""}
124
120
  />
125
121
  </PaginationItem>
126
122
  </PaginationContent>
123
+
124
+ <span className="hidden sm:block text-sm text-muted-foreground">
125
+ {t("paginationPage", {
126
+ page: pageNumber,
127
+ pageCount,
128
+ })}
129
+ </span>
127
130
  </PaginationUI>
128
131
  </div>
129
132
  )