@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.
- package/README.md +73 -73
- package/package.json +250 -218
- package/src/article/article-action-bar.tsx +110 -110
- package/src/article/article-content.tsx +18 -46
- package/src/autocomplete.tsx +201 -201
- package/src/breadcrumb.tsx +124 -124
- package/src/carousel/carousel.tsx +353 -353
- package/src/check-article-lang.tsx +47 -47
- package/src/directoryNodes/directory-tree-context.tsx +388 -0
- package/src/directoryNodes/tree-of-content.tsx +68 -67
- package/src/documents/result-list.tsx +124 -127
- package/src/favorites/bookmark-button.tsx +97 -94
- package/src/favorites/favorite-button.tsx +137 -120
- package/src/footer/footer-shell.tsx +52 -0
- package/src/footer/footer.tsx +7 -0
- package/src/footer/legal-links-block.tsx +25 -0
- package/src/footer/organization-contact-block.tsx +94 -0
- package/src/footer/social-links-block.tsx +38 -0
- package/src/footer/types.ts +10 -0
- package/src/footer/vcard-footer.tsx +72 -0
- package/src/generated/client-components.tsx +1366 -1350
- package/src/generated/create-client-request.tsx +116 -113
- package/src/generated/create-server-request.tsx +70 -61
- package/src/generated/create-suggestions-request.tsx +55 -55
- package/src/generated/server-components.tsx +1056 -1056
- package/src/generated/suggestions.tsx +302 -299
- package/src/icons/file-icon.tsx +8 -8
- package/src/icons/flag-icon.tsx +15 -15
- package/src/icons/loading.tsx +11 -11
- package/src/icons/social-icon.tsx +24 -0
- package/src/info/info-card.tsx +43 -0
- package/src/info/{info-table.tsx → information-unit-metadata-grid.tsx} +157 -168
- package/src/info/shared.tsx +49 -25
- package/src/navbar/language-switcher/content-language-switch.tsx +92 -92
- package/src/navbar/language-switcher/shared.tsx +33 -33
- package/src/navbar/language-switcher/ui-language-switch.tsx +37 -37
- package/src/navbar/navbar.tsx +157 -152
- package/src/navbar/settings.tsx +62 -62
- package/src/navbar/sign-in-out-btns.tsx +35 -35
- package/src/navbar/user-menu.tsx +60 -60
- package/src/page-wrapper.tsx +54 -31
- package/src/render-article.module.css +155 -0
- package/src/render-article.tsx +75 -68
- package/src/renditions/file-download.tsx +83 -83
- package/src/renditions/html.tsx +64 -64
- package/src/renditions/image/container.tsx +54 -54
- package/src/renditions/image/rendition.tsx +55 -55
- package/src/restriction-menu/restriction-menu-container.tsx +117 -53
- package/src/restriction-menu/restriction-menu-item.tsx +155 -147
- package/src/restriction-menu/restriction-menu.tsx +341 -156
- package/src/results/dialog-filter.tsx +166 -166
- package/src/results/empty.tsx +15 -15
- package/src/results/filter-navbar.tsx +294 -261
- package/src/results/filter-sidebar/__tests__/utils.test.ts +129 -0
- package/src/results/filter-sidebar/index.tsx +270 -126
- package/src/results/filter-sidebar/utils.ts +196 -164
- package/src/results/generic/table-result-list.tsx +97 -99
- package/src/results/{table-with-images.tsx → information-unit-search-results-card-list.tsx} +125 -127
- package/src/results/{cards.tsx → information-unit-search-results-cards.tsx} +99 -99
- package/src/results/{table.tsx → information-unit-search-results-table.tsx} +104 -104
- package/src/results/pagination.tsx +81 -81
- package/src/results/summary.ts +30 -0
- package/src/results/utils.ts +54 -54
- package/src/search-input.tsx +70 -70
- package/src/share-button.tsx +49 -49
- package/src/stores/favorites-store.ts +88 -88
- package/src/stores/highlight-store.ts +15 -15
- package/src/stores/language-store.ts +14 -14
- package/src/stores/restriction-store.ts +11 -11
- package/src/stores/search-settings-store.ts +68 -64
- 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
|
+
};
|