@c-rex/components 0.3.0-build.28 → 0.3.0-build.30
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/package.json +34 -2
- package/src/carousel/carousel.tsx +70 -74
- package/src/carousel/information-unit-carousel-item.tsx +85 -0
- package/src/directoryNodes/directory-tree-context.tsx +3 -1
- package/src/documents/description-preview.tsx +132 -0
- package/src/documents/result-list-item.tsx +256 -0
- package/src/documents/result-list.tsx +13 -132
- package/src/info/information-unit-fragment-ids.ts +28 -0
- package/src/info/information-unit-metadata-grid-client.tsx +368 -0
- package/src/info/information-unit-preview-image.tsx +77 -0
- package/src/page-wrapper.tsx +5 -5
- package/src/renditions/file-download.tsx +5 -3
- package/src/renditions/html-client.tsx +99 -0
- package/src/renditions/image/container.tsx +19 -13
- package/src/renditions/image/rendition.tsx +6 -1
- package/src/restriction-menu/restriction-menu-container.tsx +4 -117
- package/src/restriction-menu/restriction-menu.tsx +4 -381
- package/src/restriction-menu/restriction-selection-menu.tsx +383 -0
- package/src/restriction-menu/taxonomy-restriction-menu.tsx +114 -0
- package/src/results/generic/search-results-client.tsx +258 -0
- package/src/results/generic/table-result-list.tsx +5 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c-rex/components",
|
|
3
|
-
"version": "0.3.0-build.
|
|
3
|
+
"version": "0.3.0-build.30",
|
|
4
4
|
"files": [
|
|
5
5
|
"src"
|
|
6
6
|
],
|
|
@@ -53,6 +53,10 @@
|
|
|
53
53
|
"types": "./src/info/information-unit-metadata-grid.tsx",
|
|
54
54
|
"import": "./src/info/information-unit-metadata-grid.tsx"
|
|
55
55
|
},
|
|
56
|
+
"./information-unit-metadata-grid-client": {
|
|
57
|
+
"types": "./src/info/information-unit-metadata-grid-client.tsx",
|
|
58
|
+
"import": "./src/info/information-unit-metadata-grid-client.tsx"
|
|
59
|
+
},
|
|
56
60
|
"./info-card": {
|
|
57
61
|
"types": "./src/info/info-card.tsx",
|
|
58
62
|
"import": "./src/info/info-card.tsx"
|
|
@@ -73,6 +77,10 @@
|
|
|
73
77
|
"types": "./src/renditions/html.tsx",
|
|
74
78
|
"import": "./src/renditions/html.tsx"
|
|
75
79
|
},
|
|
80
|
+
"./html-rendition-client": {
|
|
81
|
+
"types": "./src/renditions/html-client.tsx",
|
|
82
|
+
"import": "./src/renditions/html-client.tsx"
|
|
83
|
+
},
|
|
76
84
|
"./image-rendition-container": {
|
|
77
85
|
"types": "./src/renditions/container.ts",
|
|
78
86
|
"import": "./src/renditions/container.ts"
|
|
@@ -169,6 +177,10 @@
|
|
|
169
177
|
"types": "./src/results/generic/table-result-list.tsx",
|
|
170
178
|
"import": "./src/results/generic/table-result-list.tsx"
|
|
171
179
|
},
|
|
180
|
+
"./generic-search-results-client": {
|
|
181
|
+
"types": "./src/results/generic/search-results-client.tsx",
|
|
182
|
+
"import": "./src/results/generic/search-results-client.tsx"
|
|
183
|
+
},
|
|
172
184
|
"./carousel": {
|
|
173
185
|
"types": "./src/carousel/carousel.tsx",
|
|
174
186
|
"import": "./src/carousel/carousel.tsx"
|
|
@@ -192,7 +204,27 @@
|
|
|
192
204
|
"./footer": {
|
|
193
205
|
"types": "./src/footer/footer.tsx",
|
|
194
206
|
"import": "./src/footer/footer.tsx"
|
|
195
|
-
}
|
|
207
|
+
},
|
|
208
|
+
"./information-unit-carousel-item": {
|
|
209
|
+
"types": "./src/carousel/information-unit-carousel-item.tsx",
|
|
210
|
+
"import": "./src/carousel/information-unit-carousel-item.tsx"
|
|
211
|
+
},
|
|
212
|
+
"./information-unit-fragment-ids": {
|
|
213
|
+
"types": "./src/info/information-unit-fragment-ids.ts",
|
|
214
|
+
"import": "./src/info/information-unit-fragment-ids.ts"
|
|
215
|
+
},
|
|
216
|
+
"./information-unit-preview-image": {
|
|
217
|
+
"types": "./src/info/information-unit-preview-image.tsx",
|
|
218
|
+
"import": "./src/info/information-unit-preview-image.tsx"
|
|
219
|
+
},
|
|
220
|
+
"./restriction-selection-menu": {
|
|
221
|
+
"types": "./src/restriction-menu/restriction-selection-menu.tsx",
|
|
222
|
+
"import": "./src/restriction-menu/restriction-selection-menu.tsx"
|
|
223
|
+
},
|
|
224
|
+
"./taxonomy-restriction-menu": {
|
|
225
|
+
"types": "./src/restriction-menu/taxonomy-restriction-menu.tsx",
|
|
226
|
+
"import": "./src/restriction-menu/taxonomy-restriction-menu.tsx"
|
|
227
|
+
}
|
|
196
228
|
},
|
|
197
229
|
"scripts": {
|
|
198
230
|
"storybook": "storybook dev -p 6006",
|
|
@@ -9,31 +9,30 @@ import {
|
|
|
9
9
|
createContext,
|
|
10
10
|
useContext,
|
|
11
11
|
} from "react";
|
|
12
|
-
import { cn
|
|
12
|
+
import { cn } from "@c-rex/utils";
|
|
13
13
|
import { Button } from "@c-rex/ui/button";
|
|
14
14
|
import { ArrowLeft, ArrowRight, LoaderCircle } from "lucide-react";
|
|
15
15
|
import { Skeleton } from "@c-rex/ui/skeleton";
|
|
16
16
|
import * as ServiceOptions from "@c-rex/services/client-requests";
|
|
17
17
|
import { documentsGetAllClientService } from "@c-rex/services/client-requests";
|
|
18
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
19
|
import { Empty } from "../results/empty";
|
|
24
|
-
import {
|
|
25
|
-
import Link from "next/link";
|
|
20
|
+
import { useTranslations } from "next-intl";
|
|
26
21
|
import { useBreakpoint } from "@c-rex/ui/hooks";
|
|
27
22
|
import { DEVICE_OPTIONS } from "@c-rex/constants";
|
|
28
23
|
import { Alert, AlertDescription, AlertTitle } from "@c-rex/ui/alert";
|
|
29
|
-
import { Tooltip, TooltipContent, TooltipTrigger } from "@c-rex/ui/tooltip";
|
|
30
24
|
import { toast } from "sonner";
|
|
25
|
+
import { InformationUnitCarouselItem } from "./information-unit-carousel-item";
|
|
26
|
+
import { Card } from "@c-rex/ui/card";
|
|
31
27
|
|
|
32
28
|
type PageInfo = {
|
|
33
29
|
hasNextPage: boolean;
|
|
34
30
|
hasPreviousPage: boolean;
|
|
35
31
|
pageCount: number;
|
|
36
32
|
};
|
|
33
|
+
|
|
34
|
+
type CarouselQueryParams = Record<string, unknown>;
|
|
35
|
+
|
|
37
36
|
type Props = {
|
|
38
37
|
className?: string;
|
|
39
38
|
arrows?: boolean;
|
|
@@ -54,10 +53,11 @@ type Props = {
|
|
|
54
53
|
imageFragmentSubjectIds?: string[];
|
|
55
54
|
}>;
|
|
56
55
|
serviceType: keyof typeof ServiceOptions;
|
|
57
|
-
queryParams?:
|
|
56
|
+
queryParams?: CarouselQueryParams;
|
|
58
57
|
loadByPages?: boolean;
|
|
59
58
|
linkPattern: string;
|
|
60
59
|
imageFragmentSubjectIds?: string[];
|
|
60
|
+
deferUntilVisible?: boolean;
|
|
61
61
|
};
|
|
62
62
|
type CarouselContextProps = {
|
|
63
63
|
next: () => void;
|
|
@@ -77,6 +77,33 @@ type CarouselContextProps = {
|
|
|
77
77
|
|
|
78
78
|
const CarouselContext = createContext<CarouselContextProps | null>(null);
|
|
79
79
|
|
|
80
|
+
const CarouselSkeletonTrack: FC<{ slidesToShow: number }> = ({ slidesToShow }) => (
|
|
81
|
+
<div className="flex w-full">
|
|
82
|
+
{Array.from({ length: slidesToShow }).map((_, index) => (
|
|
83
|
+
<div
|
|
84
|
+
key={`carousel-skeleton-${index}`}
|
|
85
|
+
className="flex-shrink-0 flex-grow-0 flex justify-center p-2"
|
|
86
|
+
style={{ width: `${100 / slidesToShow}%` }}
|
|
87
|
+
>
|
|
88
|
+
<Card className="p-4 flex-1 justify-between relative">
|
|
89
|
+
<div className="h-48 w-full overflow-hidden">
|
|
90
|
+
<Skeleton className="h-48 w-full" />
|
|
91
|
+
</div>
|
|
92
|
+
<div className="h-44 flex flex-col gap-3 pt-4">
|
|
93
|
+
<Skeleton className="h-6 w-full" />
|
|
94
|
+
<Skeleton className="h-6 w-5/6" />
|
|
95
|
+
<Skeleton className="h-6 w-2/3" />
|
|
96
|
+
</div>
|
|
97
|
+
<div className="flex justify-between w-full">
|
|
98
|
+
<Skeleton className="h-5 w-8" />
|
|
99
|
+
<Skeleton className="h-5 w-24" />
|
|
100
|
+
</div>
|
|
101
|
+
</Card>
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
|
|
80
107
|
export function useCarousel() {
|
|
81
108
|
const context = useContext(CarouselContext);
|
|
82
109
|
if (!context) {
|
|
@@ -103,6 +130,7 @@ export const Carousel: FC<Props> = ({
|
|
|
103
130
|
loadByPages = false,
|
|
104
131
|
linkPattern,
|
|
105
132
|
imageFragmentSubjectIds = [],
|
|
133
|
+
deferUntilVisible = false,
|
|
106
134
|
}) => {
|
|
107
135
|
const service = ServiceOptions[serviceType] as typeof documentsGetAllClientService;
|
|
108
136
|
const RenderComponent = carouselItemComponent || DefaultRenderCarouselItem;
|
|
@@ -110,12 +138,33 @@ export const Carousel: FC<Props> = ({
|
|
|
110
138
|
const device = useBreakpoint();
|
|
111
139
|
const t = useTranslations("results");
|
|
112
140
|
const [slidesToShow, setSlidesToShow] = useState(1);
|
|
141
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
142
|
+
const [canLoad, setCanLoad] = useState(!deferUntilVisible);
|
|
113
143
|
|
|
114
144
|
useEffect(() => {
|
|
115
145
|
if (device == null) return;
|
|
116
146
|
setSlidesToShow(itemsByRow[device as keyof typeof DEVICE_OPTIONS] as number);
|
|
117
147
|
}, [device, itemsByRow]);
|
|
118
148
|
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!deferUntilVisible || canLoad) return;
|
|
151
|
+
const node = containerRef.current;
|
|
152
|
+
if (!node) return;
|
|
153
|
+
|
|
154
|
+
const observer = new IntersectionObserver(
|
|
155
|
+
(entries) => {
|
|
156
|
+
const firstEntry = entries[0];
|
|
157
|
+
if (!firstEntry?.isIntersecting) return;
|
|
158
|
+
setCanLoad(true);
|
|
159
|
+
observer.disconnect();
|
|
160
|
+
},
|
|
161
|
+
{ rootMargin: "200px 0px", threshold: 0.05 }
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
observer.observe(node);
|
|
165
|
+
return () => observer.disconnect();
|
|
166
|
+
}, [deferUntilVisible, canLoad]);
|
|
167
|
+
|
|
119
168
|
// State
|
|
120
169
|
const [current, setCurrent] = useState(0);
|
|
121
170
|
const [data, setData] = useState<CommonItemsModel[] | null>(null);
|
|
@@ -133,6 +182,9 @@ export const Carousel: FC<Props> = ({
|
|
|
133
182
|
|
|
134
183
|
// Data loading
|
|
135
184
|
useEffect(() => {
|
|
185
|
+
if (!canLoad) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
136
188
|
let isMounted = true;
|
|
137
189
|
const controller = new AbortController();
|
|
138
190
|
setIsLoading(true);
|
|
@@ -174,7 +226,7 @@ export const Carousel: FC<Props> = ({
|
|
|
174
226
|
isMounted = false;
|
|
175
227
|
controller.abort();
|
|
176
228
|
};
|
|
177
|
-
}, [loadByPages, page, queryParams, service]);
|
|
229
|
+
}, [canLoad, loadByPages, page, queryParams, service]);
|
|
178
230
|
|
|
179
231
|
// Page info
|
|
180
232
|
useEffect(() => {
|
|
@@ -243,7 +295,7 @@ export const Carousel: FC<Props> = ({
|
|
|
243
295
|
return (
|
|
244
296
|
<CarouselContext.Provider value={contextValue}>
|
|
245
297
|
<div className={cn("flex items-center flex-col", className)}>
|
|
246
|
-
<div className={cn("w-full flex items-center")}>
|
|
298
|
+
<div ref={containerRef} className={cn("w-full flex items-center")}>
|
|
247
299
|
{(arrows && data && data.length > 0) && <CarouselPrev />}
|
|
248
300
|
<div className={cn("flex-1 overflow-hidden relative flex items-center")}>
|
|
249
301
|
{loadByPages && loading && data && data.length > 0 && (
|
|
@@ -252,11 +304,14 @@ export const Carousel: FC<Props> = ({
|
|
|
252
304
|
</div>
|
|
253
305
|
)}
|
|
254
306
|
<div
|
|
255
|
-
className="flex will-change-transform transition-all duration-600
|
|
256
|
-
style={{
|
|
307
|
+
className="flex will-change-transform transition-all duration-600 w-full"
|
|
308
|
+
style={{
|
|
309
|
+
transform: `translateX(-${(current * 100) / slidesToShow}%)`,
|
|
310
|
+
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
311
|
+
}}
|
|
257
312
|
>
|
|
258
313
|
{loading && (!data || data.length === 0) ? (
|
|
259
|
-
<
|
|
314
|
+
<CarouselSkeletonTrack slidesToShow={slidesToShow} />
|
|
260
315
|
) : error ? (
|
|
261
316
|
<div className="w-full min-h-80 flex items-center">
|
|
262
317
|
<Alert variant="destructive" className="my-2">
|
|
@@ -410,63 +465,4 @@ const DefaultRenderCarouselItem: FC<{
|
|
|
410
465
|
loadImage: boolean;
|
|
411
466
|
linkPattern: string;
|
|
412
467
|
imageFragmentSubjectIds?: string[];
|
|
413
|
-
}> = (
|
|
414
|
-
const locale = useLocale();
|
|
415
|
-
const messages = useMessages() as Record<string, unknown>;
|
|
416
|
-
const t = useTranslations("itemTypes");
|
|
417
|
-
|
|
418
|
-
const date = formatDateToLocale(item.created!, locale);
|
|
419
|
-
const title = getTitle(item.titles, item.labels);
|
|
420
|
-
const itemType = getType(item.class, locale);
|
|
421
|
-
const itemTypeKey = itemType?.toLowerCase();
|
|
422
|
-
const itemTypeMessages = (messages.itemTypes || {}) as Record<string, string>;
|
|
423
|
-
const itemTypeLabel = itemTypeKey && itemTypeMessages[itemTypeKey]
|
|
424
|
-
? t(itemTypeKey)
|
|
425
|
-
: itemType;
|
|
426
|
-
const language = getLanguage(item.languages)
|
|
427
|
-
const countryCode = language.split("-")[1] || "";
|
|
428
|
-
const link = linkPattern.replace("{shortId}", item.shortId!);
|
|
429
|
-
const previewFragmentShortId = findRelatedFragmentShortId({
|
|
430
|
-
item,
|
|
431
|
-
informationSubjectIds: imageFragmentSubjectIds,
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
return (
|
|
435
|
-
<Link href={link} className="group p-2 flex flex-1">
|
|
436
|
-
<Card className={cn("p-4 flex-1 justify-between relative", !showImages && "min-h-0")}>
|
|
437
|
-
{itemType && (
|
|
438
|
-
<Badge className="absolute -top-2 -right-2">{itemTypeLabel}</Badge>
|
|
439
|
-
)}
|
|
440
|
-
|
|
441
|
-
{showImages ? (
|
|
442
|
-
<div className="h-48 w-full overflow-hidden">
|
|
443
|
-
<ImageRenditionContainer
|
|
444
|
-
fragmentShortId={loadImage ? previewFragmentShortId : undefined}
|
|
445
|
-
emptyImageStyle="h-48 w-full"
|
|
446
|
-
skeletonStyle="h-48 w-full"
|
|
447
|
-
imageStyle="object-contain object-top h-48 !w-auto max-w-full mx-auto"
|
|
448
|
-
/>
|
|
449
|
-
</div>
|
|
450
|
-
) : null}
|
|
451
|
-
|
|
452
|
-
<Tooltip>
|
|
453
|
-
<TooltipTrigger asChild>
|
|
454
|
-
<div className="h-44 overflow-hidden">
|
|
455
|
-
<span className="line-clamp-5 text-lg font-semibold group-hover:underline">
|
|
456
|
-
{title}
|
|
457
|
-
</span>
|
|
458
|
-
</div>
|
|
459
|
-
</TooltipTrigger>
|
|
460
|
-
<TooltipContent className="max-w-sm break-words">{title}</TooltipContent>
|
|
461
|
-
</Tooltip>
|
|
462
|
-
|
|
463
|
-
<div className="flex justify-between w-full">
|
|
464
|
-
<span className="w-8 block">
|
|
465
|
-
<Flag countryCode={countryCode} />
|
|
466
|
-
</span>
|
|
467
|
-
<span className="text-gray-400">{date || item.revision}</span>
|
|
468
|
-
</div>
|
|
469
|
-
</Card>
|
|
470
|
-
</Link>
|
|
471
|
-
);
|
|
472
|
-
};
|
|
468
|
+
}> = (props) => <InformationUnitCarouselItem {...props} />;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { FC } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useLocale, useMessages, useTranslations } from "next-intl";
|
|
6
|
+
import { Card } from "@c-rex/ui/card";
|
|
7
|
+
import { Badge } from "@c-rex/ui/badge";
|
|
8
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@c-rex/ui/tooltip";
|
|
9
|
+
import type { CommonItemsModel } from "@c-rex/interfaces";
|
|
10
|
+
import { Flag } from "../icons/flag-icon";
|
|
11
|
+
import { InformationUnitPreviewImage } from "../info/information-unit-preview-image";
|
|
12
|
+
import { cn, formatDateToLocale, getLanguage, getTitle, getType } from "@c-rex/utils";
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
item: CommonItemsModel;
|
|
16
|
+
showImages: boolean;
|
|
17
|
+
loadImage: boolean;
|
|
18
|
+
linkPattern: string;
|
|
19
|
+
imageFragmentSubjectIds?: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const InformationUnitCarouselItem: FC<Props> = ({
|
|
23
|
+
item,
|
|
24
|
+
showImages,
|
|
25
|
+
loadImage,
|
|
26
|
+
linkPattern,
|
|
27
|
+
imageFragmentSubjectIds = [],
|
|
28
|
+
}) => {
|
|
29
|
+
const locale = useLocale();
|
|
30
|
+
const messages = useMessages() as Record<string, unknown>;
|
|
31
|
+
const t = useTranslations("itemTypes");
|
|
32
|
+
|
|
33
|
+
const date = formatDateToLocale(item.created!, locale);
|
|
34
|
+
const title = getTitle(item.titles, item.labels);
|
|
35
|
+
const itemType = getType(item.class, locale);
|
|
36
|
+
const itemTypeKey = itemType?.toLowerCase();
|
|
37
|
+
const itemTypeMessages = (messages.itemTypes || {}) as Record<string, string>;
|
|
38
|
+
const itemTypeLabel = itemTypeKey && itemTypeMessages[itemTypeKey]
|
|
39
|
+
? t(itemTypeKey)
|
|
40
|
+
: itemType;
|
|
41
|
+
const language = getLanguage(item.languages);
|
|
42
|
+
const countryCode = language.split("-")[1] || "";
|
|
43
|
+
const link = linkPattern.replace("{shortId}", item.shortId!);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Link href={link} className="group p-2 flex flex-1">
|
|
47
|
+
<Card className={cn("p-4 flex-1 justify-between relative", !showImages && "min-h-0")}>
|
|
48
|
+
{itemType && (
|
|
49
|
+
<Badge className="absolute -top-2 -right-2">{itemTypeLabel}</Badge>
|
|
50
|
+
)}
|
|
51
|
+
|
|
52
|
+
{showImages ? (
|
|
53
|
+
<div className="h-48 w-full overflow-hidden">
|
|
54
|
+
<InformationUnitPreviewImage
|
|
55
|
+
item={item}
|
|
56
|
+
imageFragmentSubjectIds={imageFragmentSubjectIds}
|
|
57
|
+
loadImage={loadImage}
|
|
58
|
+
emptyImageStyle="h-48 w-full"
|
|
59
|
+
skeletonStyle="h-48 w-full"
|
|
60
|
+
imageStyle="object-contain object-top h-48 !w-auto max-w-full mx-auto"
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
) : null}
|
|
64
|
+
|
|
65
|
+
<Tooltip>
|
|
66
|
+
<TooltipTrigger asChild>
|
|
67
|
+
<div className="h-44 overflow-hidden">
|
|
68
|
+
<span className="line-clamp-5 text-lg font-semibold group-hover:underline">
|
|
69
|
+
{title}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
</TooltipTrigger>
|
|
73
|
+
<TooltipContent className="max-w-sm break-words">{title}</TooltipContent>
|
|
74
|
+
</Tooltip>
|
|
75
|
+
|
|
76
|
+
<div className="flex justify-between w-full">
|
|
77
|
+
<span className="w-8 block">
|
|
78
|
+
<Flag countryCode={countryCode} />
|
|
79
|
+
</span>
|
|
80
|
+
<span className="text-gray-400">{date || item.revision}</span>
|
|
81
|
+
</div>
|
|
82
|
+
</Card>
|
|
83
|
+
</Link>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -260,11 +260,13 @@ export const DirectoryTreeSidebarMenu: FC<DirectoryTreeSidebarMenuProps> = ({
|
|
|
260
260
|
|
|
261
261
|
if (isLoading && tree.length === 0) {
|
|
262
262
|
return (
|
|
263
|
-
<div className="pt-4">
|
|
263
|
+
<div className="pt-4 space-y-2">
|
|
264
264
|
<Skeleton className="w-auto h-10 mb-2" />
|
|
265
265
|
<Skeleton className="w-auto h-10 mb-2" />
|
|
266
266
|
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
267
267
|
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
268
|
+
<Skeleton className="w-auto h-10 mb-2" />
|
|
269
|
+
<div className="px-2 pt-1 text-xs text-muted-foreground">Inhaltsverzeichnis wird geladen...</div>
|
|
268
270
|
</div>
|
|
269
271
|
);
|
|
270
272
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FC, useEffect, useState } from "react";
|
|
4
|
+
import { FragmentsGetByIdClient } from "../generated/client-components";
|
|
5
|
+
import { ExpandableSummary } from "./expandable-summary";
|
|
6
|
+
import type { RenditionModel } from "@c-rex/interfaces";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
fragmentShortId?: string;
|
|
10
|
+
title: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const htmlFormats = ["application/xhtml+xml", "application/html", "text/html"];
|
|
14
|
+
|
|
15
|
+
const getViewHref = (renditions?: RenditionModel[] | null): string | undefined => {
|
|
16
|
+
if (!renditions || renditions.length === 0) return undefined;
|
|
17
|
+
const htmlRendition = renditions.find((item) => item.format && htmlFormats.includes(item.format));
|
|
18
|
+
return htmlRendition?.links?.find((item) => item.rel === "view")?.href || undefined;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const extractSummaryText = (html: string, title: string) => {
|
|
22
|
+
if (typeof window === "undefined") return "";
|
|
23
|
+
|
|
24
|
+
const parser = new DOMParser();
|
|
25
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
26
|
+
const rawText = (doc.body?.textContent || "").replace(/\s+/g, " ").trim();
|
|
27
|
+
const withoutCdata = rawText
|
|
28
|
+
.replace(/\/\/<!\[CDATA\[/g, "")
|
|
29
|
+
.replace(/\/\/\]\]>/g, "")
|
|
30
|
+
.replace(/<!\[CDATA\[/g, "")
|
|
31
|
+
.replace(/\]\]>/g, "")
|
|
32
|
+
.trim();
|
|
33
|
+
const withoutRepeatedTitle = withoutCdata.startsWith(title)
|
|
34
|
+
? withoutCdata.slice(title.length).trim()
|
|
35
|
+
: withoutCdata;
|
|
36
|
+
|
|
37
|
+
return withoutRepeatedTitle.replace(/^\/{2,}\s*/, "").trim();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const DocumentDescriptionPreviewContent: FC<{
|
|
41
|
+
viewHref?: string;
|
|
42
|
+
title: string;
|
|
43
|
+
isLoading: boolean;
|
|
44
|
+
hasError: boolean;
|
|
45
|
+
}> = ({
|
|
46
|
+
viewHref,
|
|
47
|
+
title,
|
|
48
|
+
isLoading,
|
|
49
|
+
hasError,
|
|
50
|
+
}) => {
|
|
51
|
+
const [text, setText] = useState<string | null>(null);
|
|
52
|
+
const [loadingText, setLoadingText] = useState(false);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!viewHref) {
|
|
56
|
+
setText(null);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let isMounted = true;
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
setLoadingText(true);
|
|
63
|
+
|
|
64
|
+
fetch(viewHref, { signal: controller.signal })
|
|
65
|
+
.then((response) => response.text())
|
|
66
|
+
.then((html) => {
|
|
67
|
+
if (!isMounted || controller.signal.aborted) return;
|
|
68
|
+
setText(extractSummaryText(html, title));
|
|
69
|
+
})
|
|
70
|
+
.catch(() => {
|
|
71
|
+
if (!isMounted || controller.signal.aborted) return;
|
|
72
|
+
setText("");
|
|
73
|
+
})
|
|
74
|
+
.finally(() => {
|
|
75
|
+
if (isMounted && !controller.signal.aborted) {
|
|
76
|
+
setLoadingText(false);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
isMounted = false;
|
|
82
|
+
controller.abort();
|
|
83
|
+
};
|
|
84
|
+
}, [title, viewHref]);
|
|
85
|
+
|
|
86
|
+
if (hasError) {
|
|
87
|
+
return <span className="text-muted-foreground">No rendition available</span>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (isLoading || loadingText) {
|
|
91
|
+
return <span className="text-muted-foreground">Loading rendition...</span>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!text) {
|
|
95
|
+
return <span className="text-muted-foreground">No rendition available</span>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return <ExpandableSummary text={text} />;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const DocumentDescriptionPreview: FC<Props> = ({
|
|
102
|
+
fragmentShortId,
|
|
103
|
+
title,
|
|
104
|
+
}) => {
|
|
105
|
+
if (!fragmentShortId) {
|
|
106
|
+
return <span className="text-muted-foreground">No rendition available</span>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<FragmentsGetByIdClient
|
|
111
|
+
pathParams={{ id: fragmentShortId }}
|
|
112
|
+
queryParams={{
|
|
113
|
+
Fields: ["titles", "renditions"],
|
|
114
|
+
Embed: ["renditions"],
|
|
115
|
+
Links: true,
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{({ data, isLoading, error }) => {
|
|
119
|
+
const viewHref = getViewHref(data?.renditions);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<DocumentDescriptionPreviewContent
|
|
123
|
+
viewHref={viewHref}
|
|
124
|
+
title={title}
|
|
125
|
+
isLoading={isLoading}
|
|
126
|
+
hasError={Boolean(error)}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
}}
|
|
130
|
+
</FragmentsGetByIdClient>
|
|
131
|
+
);
|
|
132
|
+
};
|