@c-rex/components 0.1.38 → 0.1.39

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 (71) hide show
  1. package/README.md +73 -73
  2. package/package.json +250 -218
  3. package/src/article/article-action-bar.tsx +110 -110
  4. package/src/article/article-content.tsx +18 -46
  5. package/src/autocomplete.tsx +201 -201
  6. package/src/breadcrumb.tsx +124 -124
  7. package/src/carousel/carousel.tsx +353 -353
  8. package/src/check-article-lang.tsx +47 -47
  9. package/src/directoryNodes/directory-tree-context.tsx +388 -0
  10. package/src/directoryNodes/tree-of-content.tsx +68 -67
  11. package/src/documents/result-list.tsx +124 -127
  12. package/src/favorites/bookmark-button.tsx +97 -94
  13. package/src/favorites/favorite-button.tsx +137 -120
  14. package/src/footer/footer-shell.tsx +52 -0
  15. package/src/footer/footer.tsx +7 -0
  16. package/src/footer/legal-links-block.tsx +25 -0
  17. package/src/footer/organization-contact-block.tsx +94 -0
  18. package/src/footer/social-links-block.tsx +38 -0
  19. package/src/footer/types.ts +10 -0
  20. package/src/footer/vcard-footer.tsx +72 -0
  21. package/src/generated/client-components.tsx +1366 -1350
  22. package/src/generated/create-client-request.tsx +116 -113
  23. package/src/generated/create-server-request.tsx +70 -61
  24. package/src/generated/create-suggestions-request.tsx +55 -55
  25. package/src/generated/server-components.tsx +1056 -1056
  26. package/src/generated/suggestions.tsx +302 -299
  27. package/src/icons/file-icon.tsx +8 -8
  28. package/src/icons/flag-icon.tsx +15 -15
  29. package/src/icons/loading.tsx +11 -11
  30. package/src/icons/social-icon.tsx +24 -0
  31. package/src/info/info-card.tsx +43 -0
  32. package/src/info/{info-table.tsx → information-unit-metadata-grid.tsx} +157 -168
  33. package/src/info/shared.tsx +49 -25
  34. package/src/navbar/language-switcher/content-language-switch.tsx +92 -92
  35. package/src/navbar/language-switcher/shared.tsx +33 -33
  36. package/src/navbar/language-switcher/ui-language-switch.tsx +37 -37
  37. package/src/navbar/navbar.tsx +157 -152
  38. package/src/navbar/settings.tsx +62 -62
  39. package/src/navbar/sign-in-out-btns.tsx +35 -35
  40. package/src/navbar/user-menu.tsx +60 -60
  41. package/src/page-wrapper.tsx +54 -31
  42. package/src/render-article.module.css +155 -0
  43. package/src/render-article.tsx +75 -68
  44. package/src/renditions/file-download.tsx +83 -83
  45. package/src/renditions/html.tsx +64 -64
  46. package/src/renditions/image/container.tsx +54 -54
  47. package/src/renditions/image/rendition.tsx +55 -55
  48. package/src/restriction-menu/restriction-menu-container.tsx +117 -53
  49. package/src/restriction-menu/restriction-menu-item.tsx +155 -147
  50. package/src/restriction-menu/restriction-menu.tsx +341 -156
  51. package/src/results/dialog-filter.tsx +166 -166
  52. package/src/results/empty.tsx +15 -15
  53. package/src/results/filter-navbar.tsx +294 -261
  54. package/src/results/filter-sidebar/__tests__/utils.test.ts +129 -0
  55. package/src/results/filter-sidebar/index.tsx +270 -126
  56. package/src/results/filter-sidebar/utils.ts +196 -164
  57. package/src/results/generic/table-result-list.tsx +97 -99
  58. package/src/results/{table-with-images.tsx → information-unit-search-results-card-list.tsx} +125 -127
  59. package/src/results/{cards.tsx → information-unit-search-results-cards.tsx} +99 -99
  60. package/src/results/{table.tsx → information-unit-search-results-table.tsx} +104 -104
  61. package/src/results/pagination.tsx +81 -81
  62. package/src/results/summary.ts +30 -0
  63. package/src/results/utils.ts +54 -54
  64. package/src/search-input.tsx +70 -70
  65. package/src/share-button.tsx +49 -49
  66. package/src/stores/favorites-store.ts +88 -88
  67. package/src/stores/highlight-store.ts +15 -15
  68. package/src/stores/language-store.ts +14 -14
  69. package/src/stores/restriction-store.ts +11 -11
  70. package/src/stores/search-settings-store.ts +68 -64
  71. package/src/info/set-available-versions.tsx +0 -19
@@ -1,353 +1,353 @@
1
- "use client"
2
-
3
- import {
4
- useRef,
5
- useState,
6
- useEffect,
7
- useCallback,
8
- FC,
9
- createContext,
10
- useContext,
11
- } from "react";
12
- import { cn, formatDateToLocale, getLanguage, getTitle, getType } from "@c-rex/utils";
13
- import { Button } from "@c-rex/ui/button";
14
- import { ArrowLeft, ArrowRight } from "lucide-react";
15
- import { Skeleton } from "@c-rex/ui/skeleton";
16
- import * as ServiceOptions from "@c-rex/services/client-requests";
17
- import { documentsGetAllClientService } from "@c-rex/services/client-requests";
18
- import { CommonItemsModel } from "@c-rex/interfaces";
19
- import { ImageRenditionContainer } from "../renditions/image/container";
20
- import { Card } from "@c-rex/ui/card";
21
- import { Badge } from "@c-rex/ui/badge";
22
- import { Flag } from "../icons/flag-icon"
23
- import { Empty } from "../results/empty";
24
- import { useLocale, useTranslations } from "next-intl";
25
- import Link from "next/link";
26
- import { useBreakpoint } from "@c-rex/ui/hooks";
27
- import { DEVICE_OPTIONS } from "@c-rex/constants";
28
-
29
- type PageInfo = {
30
- hasNextPage: boolean;
31
- hasPreviousPage: boolean;
32
- pageCount: number;
33
- };
34
- type Props = {
35
- className?: string;
36
- arrows?: boolean;
37
- autoplay?: boolean;
38
- autoplaySpeed?: number;
39
- itemsByRow?: {
40
- [DEVICE_OPTIONS.MOBILE]: number,
41
- [DEVICE_OPTIONS.TABLET]: number,
42
- [DEVICE_OPTIONS.DESKTOP]: number,
43
- };
44
- indicators?: boolean;
45
- showImages?: boolean;
46
- carouselItemComponent?: FC<{ item: CommonItemsModel; showImages: boolean; linkPattern: string }>;
47
- serviceType: keyof typeof ServiceOptions;
48
- queryParams?: Record<string, any>;
49
- loadByPages?: boolean;
50
- linkPattern: string;
51
- };
52
- type CarouselContextProps = {
53
- next: () => void;
54
- prev: () => void;
55
- setCurrent: (idx: number) => void;
56
- current: number;
57
- slidesToShow: number;
58
- slidesLength: number;
59
- page?: number;
60
- pageCount?: number;
61
- setPage?: (page: number) => void;
62
- hasNextPage?: boolean;
63
- hasPrevPage?: boolean;
64
- };
65
-
66
- const CarouselContext = createContext<CarouselContextProps | null>(null);
67
-
68
- export function useCarousel() {
69
- const context = useContext(CarouselContext);
70
- if (!context) {
71
- throw new Error("useCarousel must be used within a <Carousel />");
72
- }
73
- return context;
74
- }
75
-
76
- export const Carousel: FC<Props> = ({
77
- className,
78
- arrows = false,
79
- autoplay = false,
80
- itemsByRow = {
81
- [DEVICE_OPTIONS.MOBILE]: 1,
82
- [DEVICE_OPTIONS.TABLET]: 3,
83
- [DEVICE_OPTIONS.DESKTOP]: 4,
84
- },
85
- indicators = false,
86
- autoplaySpeed = 3000,
87
- showImages = false,
88
- carouselItemComponent,
89
- serviceType,
90
- queryParams = {},
91
- loadByPages = false,
92
- linkPattern
93
- }) => {
94
- const service = ServiceOptions[serviceType] as typeof documentsGetAllClientService;
95
- const RenderComponent = carouselItemComponent || DefaultRenderCarouselItem;
96
-
97
- const device = useBreakpoint();
98
- const [slidesToShow, setSlidesToShow] = useState(1);
99
-
100
- useEffect(() => {
101
- if (device == null) return;
102
- setSlidesToShow(itemsByRow[device as keyof typeof DEVICE_OPTIONS] as number);
103
- }, [device, itemsByRow]);
104
-
105
- // State
106
- const [current, setCurrent] = useState(0);
107
- const [data, setData] = useState<CommonItemsModel[] | null>(null);
108
- const [loading, setIsLoading] = useState(true);
109
- const [error, setError] = useState<unknown>(null);
110
-
111
- // Pagination
112
- const [page, setPage] = useState(1);
113
- const [pageInfo, setPageInfo] = useState<PageInfo | null>(null);
114
- const [hasNextPage, setHasNextPage] = useState(true);
115
- const [hasPrevPage, setHasPrevPage] = useState(false);
116
-
117
- // Autoplay
118
- const timer = useRef<NodeJS.Timeout | null>(null);
119
-
120
- // Data loading
121
- useEffect(() => {
122
- let isMounted = true;
123
- setIsLoading(true);
124
- setError(null);
125
- setData(null);
126
- if (loadByPages) {
127
- (async () => {
128
- try {
129
- const result = await service({ ...queryParams, PageNumber: page });
130
- if (!isMounted) return;
131
- setData((result && 'items' in result) ? result.items as CommonItemsModel[] : []);
132
- setPageInfo((result && 'pageInfo' in result) ? result.pageInfo as PageInfo : null);
133
- } catch (err) {
134
- if (!isMounted) return;
135
- setError(err);
136
- } finally {
137
- if (isMounted) setIsLoading(false);
138
- }
139
- })();
140
- } else {
141
- (async () => {
142
- try {
143
- const result = await service(queryParams);
144
- if (!isMounted) return;
145
- setData((result && 'items' in result) ? result.items as CommonItemsModel[] : []);
146
- } catch (err) {
147
- if (!isMounted) return;
148
- setError(err);
149
- } finally {
150
- if (isMounted) setIsLoading(false);
151
- }
152
- })();
153
- }
154
- return () => { isMounted = false; };
155
- }, [queryParams, page, loadByPages, service]);
156
-
157
- // Page info
158
- useEffect(() => {
159
- if (loadByPages && pageInfo) {
160
- setHasNextPage(pageInfo.hasNextPage);
161
- setHasPrevPage(pageInfo.hasPreviousPage);
162
- }
163
- }, [pageInfo, loadByPages]);
164
-
165
- // Autoplay logic (static mode only)
166
- useEffect(() => {
167
- if (!autoplay || loading || !data || data.length === 0 || loadByPages) return;
168
- timer.current = setTimeout(() => {
169
- if (current < data.length - slidesToShow) {
170
- setCurrent((prev) => prev + 1);
171
- } else {
172
- setCurrent(0);
173
- }
174
- }, autoplaySpeed);
175
- return () => {
176
- if (timer.current) clearTimeout(timer.current);
177
- };
178
- }, [autoplay, autoplaySpeed, current, data, slidesToShow, loading, loadByPages]);
179
-
180
- // Navigation
181
- const prev = useCallback(() => {
182
- if (loadByPages) {
183
- if (hasPrevPage) setPage((p) => p - 1);
184
- } else {
185
- setCurrent((prev) => Math.max(prev - 1, 0));
186
- }
187
- }, [loadByPages, hasPrevPage]);
188
-
189
- const next = useCallback(() => {
190
- if (loadByPages) {
191
- if (hasNextPage) setPage((p) => p + 1);
192
- } else {
193
- setCurrent((prev) => (prev < (data?.length || 0) - slidesToShow ? prev + 1 : 0));
194
- }
195
- }, [loadByPages, hasNextPage, data, slidesToShow]);
196
-
197
- const contextValue: CarouselContextProps = {
198
- next,
199
- prev,
200
- setCurrent,
201
- current,
202
- slidesToShow,
203
- pageCount: pageInfo?.pageCount || 1,
204
- slidesLength: data?.length || 0,
205
- page: loadByPages ? page : undefined,
206
- setPage: loadByPages ? setPage : undefined,
207
- hasNextPage: loadByPages ? hasNextPage : undefined,
208
- hasPrevPage: loadByPages ? hasPrevPage : undefined,
209
- };
210
-
211
- return (
212
- <CarouselContext.Provider value={contextValue}>
213
- <div className={cn("flex items-center flex-col", className)}>
214
- <div className={cn("w-full flex items-center")}>
215
- {(arrows && data && data.length > 0) && <CarouselPrev />}
216
- <div className={cn("flex-1 overflow-hidden relative flex items-center")}>
217
- <div
218
- className="flex will-change-transform transition-all duration-600 ease-[cubic-bezier(0.4,0,0.2,1)] w-full"
219
- style={{ transform: `translateX(-${(current * 100) / slidesToShow}%)` }}
220
- >
221
- {loading ? (
222
- <Skeleton className="w-full h-80" />
223
- ) : error ? (
224
- <div className="w-full h-80 flex items-center justify-center text-red-500">
225
- {JSON.stringify(error)}
226
- </div>
227
- ) : data && data.length == 0 ? (
228
- <Empty />
229
- ) : data && data.length > 0 ? (
230
- data.map((item: CommonItemsModel) => (
231
- <div
232
- key={item.shortId}
233
- className={`flex-shrink-0 flex-grow-0 flex justify-center`}
234
- style={{ width: `${100 / slidesToShow}%` }}
235
- >
236
- <RenderComponent item={item} showImages={showImages} linkPattern={linkPattern} />
237
- </div>
238
- ))
239
- ) : null}
240
- </div>
241
- </div>
242
- {(arrows && data && data.length > 0) && <CarouselNext />}
243
- </div>
244
- {indicators && <CarouselIndicators />}
245
- </div>
246
- </CarouselContext.Provider>
247
- );
248
- };
249
-
250
- const CarouselNext: FC = () => {
251
- const { next, current, slidesToShow, slidesLength, hasNextPage } = useCarousel();
252
- const disabled = hasNextPage === undefined ? current >= slidesLength - slidesToShow : !hasNextPage;
253
- return (
254
- <Button className="w-9" rounded="full" onClick={next} variant="default" disabled={disabled}>
255
- <ArrowRight />
256
- </Button>
257
- );
258
- };
259
-
260
- const CarouselPrev: FC = () => {
261
- const { prev, current, hasPrevPage } = useCarousel();
262
- const disabled = hasPrevPage === undefined ? current === 0 : !hasPrevPage;
263
-
264
- return (
265
- <Button className="w-9" rounded="full" onClick={prev} variant="default" disabled={disabled}>
266
- <ArrowLeft />
267
- </Button>
268
- );
269
- };
270
-
271
- const CarouselIndicators: FC<{ className?: string }> = ({ className }) => {
272
- const { current, setCurrent, slidesToShow, slidesLength, page, setPage, pageCount } = useCarousel();
273
- // Static mode
274
- if (!setPage) {
275
- const totalOfIndicators = Math.max(slidesLength - slidesToShow + 1, 1);
276
- if (totalOfIndicators === 1) return null;
277
- return (
278
- <ol className={cn("flex gap-2 z-20 list-none p-0 m-0", className)}>
279
- {Array.from({ length: totalOfIndicators }).map((_, index) => (
280
- <li key={`indicator-${index}`}>
281
- <Button
282
- className={`w-3 h-3 rounded-full border-0 cursor-pointer transition-colors ${index === current ? "bg-gray-800" : "bg-gray-300"}`}
283
- size="xs"
284
- onClick={() => setCurrent(index)}
285
- />
286
- </li>
287
- ))}
288
- </ol>
289
- );
290
- }
291
- // Paginated mode
292
- const totalOfIndicators = pageCount || 1;
293
- if (totalOfIndicators === 1) return null;
294
- return (
295
- <ol className={cn("flex gap-2 z-20 list-none p-0 m-0", className)}>
296
- {Array.from({ length: totalOfIndicators }).map((_, index) => {
297
- const pageNumber = index + 1;
298
- return (
299
- <li key={pageNumber}>
300
- <Button
301
- className={`w-3 h-3 rounded-full border-0 cursor-pointer transition-colors ${pageNumber === page ? "bg-gray-800" : "bg-gray-300"}`}
302
- size="xs"
303
- onClick={() => setPage(pageNumber)}
304
- />
305
- </li>
306
- );
307
- })}
308
- </ol>
309
- );
310
- };
311
-
312
- const DefaultRenderCarouselItem: FC<{
313
- item: CommonItemsModel;
314
- showImages: boolean;
315
- linkPattern: string
316
- }> = ({ item, showImages, linkPattern }) => {
317
- const locale = useLocale();
318
- const t = useTranslations("itemTypes");
319
-
320
- const date = formatDateToLocale(item.created!, locale);
321
- const title = getTitle(item.titles, item.labels);
322
- const itemType = getType(item.class);
323
- const language = getLanguage(item.languages)
324
- const countryCode = language.split("-")[1] || "";
325
- const link = linkPattern.replace("{shortId}", item.shortId!);
326
-
327
- return (
328
- <Link href={link} className="group p-2 flex flex-1">
329
- <Card className="p-4 flex-1 justify-between relative">
330
- <Badge className="absolute -top-2 -right-2">{t(itemType.toLowerCase())}</Badge>
331
-
332
- {showImages && (
333
- <ImageRenditionContainer
334
- itemShortId={item.shortId!}
335
- emptyImageStyle="h-48 w-full"
336
- imageStyle="object-cover h-48 w-full"
337
- />
338
- )}
339
-
340
- <span className="group-hover:underline text-lg font-semibold flex-1">
341
- {title}
342
- </span>
343
-
344
- <div className="flex justify-between w-full">
345
- <span className="w-8 block">
346
- <Flag countryCode={countryCode} />
347
- </span>
348
- <span className="text-gray-400">{date || item.revision}</span>
349
- </div>
350
- </Card>
351
- </Link>
352
- );
353
- };
1
+ "use client"
2
+
3
+ import {
4
+ useRef,
5
+ useState,
6
+ useEffect,
7
+ useCallback,
8
+ FC,
9
+ createContext,
10
+ useContext,
11
+ } from "react";
12
+ import { cn, formatDateToLocale, getLanguage, getTitle, getType } from "@c-rex/utils";
13
+ import { Button } from "@c-rex/ui/button";
14
+ import { ArrowLeft, ArrowRight } from "lucide-react";
15
+ import { Skeleton } from "@c-rex/ui/skeleton";
16
+ import * as ServiceOptions from "@c-rex/services/client-requests";
17
+ import { documentsGetAllClientService } from "@c-rex/services/client-requests";
18
+ import { CommonItemsModel } from "@c-rex/interfaces";
19
+ import { ImageRenditionContainer } from "../renditions/image/container";
20
+ import { Card } from "@c-rex/ui/card";
21
+ import { Badge } from "@c-rex/ui/badge";
22
+ import { Flag } from "../icons/flag-icon"
23
+ import { Empty } from "../results/empty";
24
+ import { useLocale, useTranslations } from "next-intl";
25
+ import Link from "next/link";
26
+ import { useBreakpoint } from "@c-rex/ui/hooks";
27
+ import { DEVICE_OPTIONS } from "@c-rex/constants";
28
+
29
+ type PageInfo = {
30
+ hasNextPage: boolean;
31
+ hasPreviousPage: boolean;
32
+ pageCount: number;
33
+ };
34
+ type Props = {
35
+ className?: string;
36
+ arrows?: boolean;
37
+ autoplay?: boolean;
38
+ autoplaySpeed?: number;
39
+ itemsByRow?: {
40
+ [DEVICE_OPTIONS.MOBILE]: number,
41
+ [DEVICE_OPTIONS.TABLET]: number,
42
+ [DEVICE_OPTIONS.DESKTOP]: number,
43
+ };
44
+ indicators?: boolean;
45
+ showImages?: boolean;
46
+ carouselItemComponent?: FC<{ item: CommonItemsModel; showImages: boolean; linkPattern: string }>;
47
+ serviceType: keyof typeof ServiceOptions;
48
+ queryParams?: Record<string, any>;
49
+ loadByPages?: boolean;
50
+ linkPattern: string;
51
+ };
52
+ type CarouselContextProps = {
53
+ next: () => void;
54
+ prev: () => void;
55
+ setCurrent: (idx: number) => void;
56
+ current: number;
57
+ slidesToShow: number;
58
+ slidesLength: number;
59
+ page?: number;
60
+ pageCount?: number;
61
+ setPage?: (page: number) => void;
62
+ hasNextPage?: boolean;
63
+ hasPrevPage?: boolean;
64
+ };
65
+
66
+ const CarouselContext = createContext<CarouselContextProps | null>(null);
67
+
68
+ export function useCarousel() {
69
+ const context = useContext(CarouselContext);
70
+ if (!context) {
71
+ throw new Error("useCarousel must be used within a <Carousel />");
72
+ }
73
+ return context;
74
+ }
75
+
76
+ export const Carousel: FC<Props> = ({
77
+ className,
78
+ arrows = false,
79
+ autoplay = false,
80
+ itemsByRow = {
81
+ [DEVICE_OPTIONS.MOBILE]: 1,
82
+ [DEVICE_OPTIONS.TABLET]: 3,
83
+ [DEVICE_OPTIONS.DESKTOP]: 4,
84
+ },
85
+ indicators = false,
86
+ autoplaySpeed = 3000,
87
+ showImages = false,
88
+ carouselItemComponent,
89
+ serviceType,
90
+ queryParams = {},
91
+ loadByPages = false,
92
+ linkPattern
93
+ }) => {
94
+ const service = ServiceOptions[serviceType] as typeof documentsGetAllClientService;
95
+ const RenderComponent = carouselItemComponent || DefaultRenderCarouselItem;
96
+
97
+ const device = useBreakpoint();
98
+ const [slidesToShow, setSlidesToShow] = useState(1);
99
+
100
+ useEffect(() => {
101
+ if (device == null) return;
102
+ setSlidesToShow(itemsByRow[device as keyof typeof DEVICE_OPTIONS] as number);
103
+ }, [device, itemsByRow]);
104
+
105
+ // State
106
+ const [current, setCurrent] = useState(0);
107
+ const [data, setData] = useState<CommonItemsModel[] | null>(null);
108
+ const [loading, setIsLoading] = useState(true);
109
+ const [error, setError] = useState<unknown>(null);
110
+
111
+ // Pagination
112
+ const [page, setPage] = useState(1);
113
+ const [pageInfo, setPageInfo] = useState<PageInfo | null>(null);
114
+ const [hasNextPage, setHasNextPage] = useState(true);
115
+ const [hasPrevPage, setHasPrevPage] = useState(false);
116
+
117
+ // Autoplay
118
+ const timer = useRef<NodeJS.Timeout | null>(null);
119
+
120
+ // Data loading
121
+ useEffect(() => {
122
+ let isMounted = true;
123
+ setIsLoading(true);
124
+ setError(null);
125
+ setData(null);
126
+ if (loadByPages) {
127
+ (async () => {
128
+ try {
129
+ const result = await service({ ...queryParams, PageNumber: page });
130
+ if (!isMounted) return;
131
+ setData((result && 'items' in result) ? result.items as CommonItemsModel[] : []);
132
+ setPageInfo((result && 'pageInfo' in result) ? result.pageInfo as PageInfo : null);
133
+ } catch (err) {
134
+ if (!isMounted) return;
135
+ setError(err);
136
+ } finally {
137
+ if (isMounted) setIsLoading(false);
138
+ }
139
+ })();
140
+ } else {
141
+ (async () => {
142
+ try {
143
+ const result = await service(queryParams);
144
+ if (!isMounted) return;
145
+ setData((result && 'items' in result) ? result.items as CommonItemsModel[] : []);
146
+ } catch (err) {
147
+ if (!isMounted) return;
148
+ setError(err);
149
+ } finally {
150
+ if (isMounted) setIsLoading(false);
151
+ }
152
+ })();
153
+ }
154
+ return () => { isMounted = false; };
155
+ }, [queryParams, page, loadByPages, service]);
156
+
157
+ // Page info
158
+ useEffect(() => {
159
+ if (loadByPages && pageInfo) {
160
+ setHasNextPage(pageInfo.hasNextPage);
161
+ setHasPrevPage(pageInfo.hasPreviousPage);
162
+ }
163
+ }, [pageInfo, loadByPages]);
164
+
165
+ // Autoplay logic (static mode only)
166
+ useEffect(() => {
167
+ if (!autoplay || loading || !data || data.length === 0 || loadByPages) return;
168
+ timer.current = setTimeout(() => {
169
+ if (current < data.length - slidesToShow) {
170
+ setCurrent((prev) => prev + 1);
171
+ } else {
172
+ setCurrent(0);
173
+ }
174
+ }, autoplaySpeed);
175
+ return () => {
176
+ if (timer.current) clearTimeout(timer.current);
177
+ };
178
+ }, [autoplay, autoplaySpeed, current, data, slidesToShow, loading, loadByPages]);
179
+
180
+ // Navigation
181
+ const prev = useCallback(() => {
182
+ if (loadByPages) {
183
+ if (hasPrevPage) setPage((p) => p - 1);
184
+ } else {
185
+ setCurrent((prev) => Math.max(prev - 1, 0));
186
+ }
187
+ }, [loadByPages, hasPrevPage]);
188
+
189
+ const next = useCallback(() => {
190
+ if (loadByPages) {
191
+ if (hasNextPage) setPage((p) => p + 1);
192
+ } else {
193
+ setCurrent((prev) => (prev < (data?.length || 0) - slidesToShow ? prev + 1 : 0));
194
+ }
195
+ }, [loadByPages, hasNextPage, data, slidesToShow]);
196
+
197
+ const contextValue: CarouselContextProps = {
198
+ next,
199
+ prev,
200
+ setCurrent,
201
+ current,
202
+ slidesToShow,
203
+ pageCount: pageInfo?.pageCount || 1,
204
+ slidesLength: data?.length || 0,
205
+ page: loadByPages ? page : undefined,
206
+ setPage: loadByPages ? setPage : undefined,
207
+ hasNextPage: loadByPages ? hasNextPage : undefined,
208
+ hasPrevPage: loadByPages ? hasPrevPage : undefined,
209
+ };
210
+
211
+ return (
212
+ <CarouselContext.Provider value={contextValue}>
213
+ <div className={cn("flex items-center flex-col", className)}>
214
+ <div className={cn("w-full flex items-center")}>
215
+ {(arrows && data && data.length > 0) && <CarouselPrev />}
216
+ <div className={cn("flex-1 overflow-hidden relative flex items-center")}>
217
+ <div
218
+ className="flex will-change-transform transition-all duration-600 ease-[cubic-bezier(0.4,0,0.2,1)] w-full"
219
+ style={{ transform: `translateX(-${(current * 100) / slidesToShow}%)` }}
220
+ >
221
+ {loading ? (
222
+ <Skeleton className="w-full h-80" />
223
+ ) : error ? (
224
+ <div className="w-full h-80 flex items-center justify-center text-red-500">
225
+ {JSON.stringify(error)}
226
+ </div>
227
+ ) : data && data.length == 0 ? (
228
+ <Empty />
229
+ ) : data && data.length > 0 ? (
230
+ data.map((item: CommonItemsModel) => (
231
+ <div
232
+ key={item.shortId}
233
+ className={`flex-shrink-0 flex-grow-0 flex justify-center`}
234
+ style={{ width: `${100 / slidesToShow}%` }}
235
+ >
236
+ <RenderComponent item={item} showImages={showImages} linkPattern={linkPattern} />
237
+ </div>
238
+ ))
239
+ ) : null}
240
+ </div>
241
+ </div>
242
+ {(arrows && data && data.length > 0) && <CarouselNext />}
243
+ </div>
244
+ {indicators && <CarouselIndicators />}
245
+ </div>
246
+ </CarouselContext.Provider>
247
+ );
248
+ };
249
+
250
+ const CarouselNext: FC = () => {
251
+ const { next, current, slidesToShow, slidesLength, hasNextPage } = useCarousel();
252
+ const disabled = hasNextPage === undefined ? current >= slidesLength - slidesToShow : !hasNextPage;
253
+ return (
254
+ <Button className="w-9" rounded="full" onClick={next} variant="default" disabled={disabled}>
255
+ <ArrowRight />
256
+ </Button>
257
+ );
258
+ };
259
+
260
+ const CarouselPrev: FC = () => {
261
+ const { prev, current, hasPrevPage } = useCarousel();
262
+ const disabled = hasPrevPage === undefined ? current === 0 : !hasPrevPage;
263
+
264
+ return (
265
+ <Button className="w-9" rounded="full" onClick={prev} variant="default" disabled={disabled}>
266
+ <ArrowLeft />
267
+ </Button>
268
+ );
269
+ };
270
+
271
+ const CarouselIndicators: FC<{ className?: string }> = ({ className }) => {
272
+ const { current, setCurrent, slidesToShow, slidesLength, page, setPage, pageCount } = useCarousel();
273
+ // Static mode
274
+ if (!setPage) {
275
+ const totalOfIndicators = Math.max(slidesLength - slidesToShow + 1, 1);
276
+ if (totalOfIndicators === 1) return null;
277
+ return (
278
+ <ol className={cn("flex gap-2 z-20 list-none p-0 m-0", className)}>
279
+ {Array.from({ length: totalOfIndicators }).map((_, index) => (
280
+ <li key={`indicator-${index}`}>
281
+ <Button
282
+ className={`w-3 h-3 rounded-full border-0 cursor-pointer transition-colors ${index === current ? "bg-gray-800" : "bg-gray-300"}`}
283
+ size="xs"
284
+ onClick={() => setCurrent(index)}
285
+ />
286
+ </li>
287
+ ))}
288
+ </ol>
289
+ );
290
+ }
291
+ // Paginated mode
292
+ const totalOfIndicators = pageCount || 1;
293
+ if (totalOfIndicators === 1) return null;
294
+ return (
295
+ <ol className={cn("flex gap-2 z-20 list-none p-0 m-0", className)}>
296
+ {Array.from({ length: totalOfIndicators }).map((_, index) => {
297
+ const pageNumber = index + 1;
298
+ return (
299
+ <li key={pageNumber}>
300
+ <Button
301
+ className={`w-3 h-3 rounded-full border-0 cursor-pointer transition-colors ${pageNumber === page ? "bg-gray-800" : "bg-gray-300"}`}
302
+ size="xs"
303
+ onClick={() => setPage(pageNumber)}
304
+ />
305
+ </li>
306
+ );
307
+ })}
308
+ </ol>
309
+ );
310
+ };
311
+
312
+ const DefaultRenderCarouselItem: FC<{
313
+ item: CommonItemsModel;
314
+ showImages: boolean;
315
+ linkPattern: string
316
+ }> = ({ item, showImages, linkPattern }) => {
317
+ const locale = useLocale();
318
+ const t = useTranslations("itemTypes");
319
+
320
+ const date = formatDateToLocale(item.created!, locale);
321
+ const title = getTitle(item.titles, item.labels);
322
+ const itemType = getType(item.class);
323
+ const language = getLanguage(item.languages)
324
+ const countryCode = language.split("-")[1] || "";
325
+ const link = linkPattern.replace("{shortId}", item.shortId!);
326
+
327
+ return (
328
+ <Link href={link} className="group p-2 flex flex-1">
329
+ <Card className="p-4 flex-1 justify-between relative">
330
+ <Badge className="absolute -top-2 -right-2">{t(itemType.toLowerCase())}</Badge>
331
+
332
+ {showImages && (
333
+ <ImageRenditionContainer
334
+ itemShortId={item.shortId!}
335
+ emptyImageStyle="h-48 w-full"
336
+ imageStyle="object-cover h-48 w-full"
337
+ />
338
+ )}
339
+
340
+ <span className="group-hover:underline text-lg font-semibold flex-1">
341
+ {title}
342
+ </span>
343
+
344
+ <div className="flex justify-between w-full">
345
+ <span className="w-8 block">
346
+ <Flag countryCode={countryCode} />
347
+ </span>
348
+ <span className="text-gray-400">{date || item.revision}</span>
349
+ </div>
350
+ </Card>
351
+ </Link>
352
+ );
353
+ };