@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.
@@ -0,0 +1,256 @@
1
+ "use client";
2
+
3
+ import { FC, useEffect, useMemo, useRef, useState } from "react";
4
+ import { CommonItemsModel } from "@c-rex/interfaces";
5
+ import { FileStack } from "lucide-react";
6
+ import { cn, findRelatedFragmentShortId, generateQueryParams } from "@c-rex/utils";
7
+ import { Button } from "@c-rex/ui/button";
8
+ import { Badge } from "@c-rex/ui/badge";
9
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@c-rex/ui/tooltip";
10
+ import { RESULT_TYPES } from "@c-rex/constants";
11
+ import { FileDownloadDropdown } from "@c-rex/components/file-download";
12
+ import { FavoriteButton } from "@c-rex/components/favorite-button";
13
+ import { ImageRenditionContainer } from "../renditions/image/container";
14
+ import { BookmarkButton } from "../favorites/bookmark-button";
15
+ import { QueryParams } from "@c-rex/types";
16
+ import { getResultItemSummary } from "../results/summary";
17
+ import { ExpandableSummary } from "./expandable-summary";
18
+ import { DocumentsGetByIdClient } from "../generated/client-components";
19
+ import { DocumentDescriptionPreview } from "./description-preview";
20
+ import { Skeleton } from "@c-rex/ui/skeleton";
21
+ import { ResultTypes } from "@c-rex/types";
22
+ import Link from "next/link";
23
+
24
+ interface Props {
25
+ item: CommonItemsModel;
26
+ query?: string;
27
+ imageFragmentSubjectIds?: string[];
28
+ descriptionFragmentSubjectIds?: string[];
29
+ isLast: boolean;
30
+ eager?: boolean;
31
+ }
32
+
33
+ type RowContentProps = {
34
+ item: CommonItemsModel;
35
+ itemLink: string;
36
+ title: string;
37
+ language: string;
38
+ itemType: ResultTypes;
39
+ multipleVersions: string[];
40
+ isDocument: boolean;
41
+ isLast: boolean;
42
+ previewFragmentShortId?: string;
43
+ descriptionFragmentShortId?: string;
44
+ isLoadingDetails?: boolean;
45
+ };
46
+
47
+ const DocumentResultListRowContent: FC<RowContentProps> = ({
48
+ item,
49
+ itemLink,
50
+ title,
51
+ language,
52
+ itemType,
53
+ multipleVersions,
54
+ isDocument,
55
+ isLast,
56
+ previewFragmentShortId,
57
+ descriptionFragmentShortId,
58
+ isLoadingDetails = false,
59
+ }) => {
60
+ return (
61
+ <div
62
+ className={cn(
63
+ "min-h-12 flex flex-wrap items-start border px-4 py-2 gap-2 rounded",
64
+ isLast ? "" : "mb-4",
65
+ `c-rex-result-item c-rex-result-${itemType}`
66
+ )}
67
+ >
68
+ <div className="w-20 sm:w-24 flex shrink-0 items-start justify-center pt-1">
69
+ {isLoadingDetails ? (
70
+ <Skeleton className="w-20 sm:w-24 aspect-[210/297]" />
71
+ ) : (
72
+ <Link href={itemLink} className="block w-20 sm:w-24">
73
+ <ImageRenditionContainer
74
+ fragmentShortId={previewFragmentShortId}
75
+ emptyImageStyle="w-20 sm:w-24 aspect-[210/297]"
76
+ imageStyle="block w-full h-auto max-w-full object-contain object-top mx-auto"
77
+ />
78
+ </Link>
79
+ )}
80
+ </div>
81
+
82
+ <div className="flex-1 p-2 flex flex-col justify-start">
83
+ <span className="text-sm text-muted-foreground">
84
+ {item.revision} - {language}
85
+ </span>
86
+
87
+ <span className="text-lg font-medium">
88
+ <a className="hover:underline" href={itemLink}>{title}</a>
89
+ </span>
90
+
91
+ <div>
92
+ <Badge>{itemType}</Badge>
93
+ </div>
94
+
95
+ <span className="text-sm block">
96
+ {isLoadingDetails ? (
97
+ <div className="flex flex-col gap-1 pt-1">
98
+ <Skeleton className="h-4 w-full" />
99
+ <Skeleton className="h-4 w-5/6" />
100
+ </div>
101
+ ) : descriptionFragmentShortId ? (
102
+ <DocumentDescriptionPreview
103
+ fragmentShortId={descriptionFragmentShortId}
104
+ title={title}
105
+ />
106
+ ) : (
107
+ <ExpandableSummary text="" />
108
+ )}
109
+ </span>
110
+ </div>
111
+
112
+ <div className="flex flex-col p-2 ml-auto justify-start self-start">
113
+ <div className="flex gap-2">
114
+ <FileDownloadDropdown renditions={item.renditions} />
115
+
116
+ <FavoriteButton
117
+ id={item.shortId!}
118
+ type={itemType}
119
+ label={title}
120
+ />
121
+
122
+ {multipleVersions.length > 1 && (
123
+ <Tooltip>
124
+ <TooltipTrigger asChild>
125
+ <Button variant="ghost" size="icon">
126
+ <FileStack />
127
+ </Button>
128
+ </TooltipTrigger>
129
+ <TooltipContent>
130
+ Available in: {multipleVersions.join(", ")}
131
+ </TooltipContent>
132
+ </Tooltip>
133
+ )}
134
+
135
+ {isDocument && (
136
+ <BookmarkButton
137
+ shortId={item.shortId!}
138
+ triggerVariant="ghost"
139
+ />
140
+ )}
141
+ </div>
142
+ </div>
143
+ </div>
144
+ );
145
+ };
146
+
147
+ export const DocumentsResultListItem: FC<Props> = ({
148
+ item,
149
+ query = "",
150
+ imageFragmentSubjectIds = [],
151
+ descriptionFragmentSubjectIds = [],
152
+ isLast,
153
+ eager = false,
154
+ }) => {
155
+ const { title, language, itemType, multipleVersions, packageId } = getResultItemSummary(item);
156
+ const isDocument = itemType === RESULT_TYPES.DOCUMENT;
157
+ const [canLoadDetails, setCanLoadDetails] = useState(eager);
158
+ const rowRef = useRef<HTMLDivElement | null>(null);
159
+ const queryParams: QueryParams[] = [];
160
+
161
+ if (packageId) {
162
+ queryParams.push({
163
+ key: "package",
164
+ value: packageId,
165
+ });
166
+ }
167
+
168
+ if (query.length > 0) {
169
+ queryParams.push({
170
+ key: "q",
171
+ value: query,
172
+ });
173
+ }
174
+
175
+ const params = generateQueryParams(queryParams);
176
+ const itemLink = `/documents/${item.shortId}` + (params.length > 0 ? `?${params}` : "");
177
+
178
+ useEffect(() => {
179
+ if (canLoadDetails) return;
180
+ const node = rowRef.current;
181
+ if (!node) return;
182
+
183
+ const observer = new IntersectionObserver(
184
+ (entries) => {
185
+ const firstEntry = entries[0];
186
+ if (!firstEntry?.isIntersecting) return;
187
+ setCanLoadDetails(true);
188
+ observer.disconnect();
189
+ },
190
+ { rootMargin: "300px 0px", threshold: 0.05 }
191
+ );
192
+
193
+ observer.observe(node);
194
+ return () => observer.disconnect();
195
+ }, [canLoadDetails]);
196
+
197
+ const baseContent = useMemo(() => (
198
+ <DocumentResultListRowContent
199
+ item={item}
200
+ itemLink={itemLink}
201
+ title={title}
202
+ language={language}
203
+ itemType={itemType}
204
+ multipleVersions={multipleVersions}
205
+ isDocument={isDocument}
206
+ isLast={isLast}
207
+ isLoadingDetails={!canLoadDetails}
208
+ />
209
+ ), [canLoadDetails, isDocument, isLast, item, itemLink, itemType, language, multipleVersions, title]);
210
+
211
+ return (
212
+ <div ref={rowRef}>
213
+ {!canLoadDetails || !item.shortId ? (
214
+ baseContent
215
+ ) : (
216
+ <DocumentsGetByIdClient
217
+ pathParams={{ id: item.shortId }}
218
+ queryParams={{
219
+ // TODO(IDS): Remove this per-row detail lookup and resolve preview/description
220
+ // directly from the list payload once `DocumentsGetAll(...Embed=informationUnits)`
221
+ // includes the required `informationUnits[*].informationSubjects` markers again.
222
+ Fields: ["informationUnits"],
223
+ Embed: ["informationUnits"],
224
+ }}
225
+ >
226
+ {({ data, isLoading }) => {
227
+ const previewFragmentShortId = data ? findRelatedFragmentShortId({
228
+ item: data,
229
+ informationSubjectIds: imageFragmentSubjectIds,
230
+ }) : undefined;
231
+ const descriptionFragmentShortId = data ? findRelatedFragmentShortId({
232
+ item: data,
233
+ informationSubjectIds: descriptionFragmentSubjectIds,
234
+ }) : undefined;
235
+
236
+ return (
237
+ <DocumentResultListRowContent
238
+ item={item}
239
+ itemLink={itemLink}
240
+ title={title}
241
+ language={language}
242
+ itemType={itemType}
243
+ multipleVersions={multipleVersions}
244
+ isDocument={isDocument}
245
+ isLast={isLast}
246
+ previewFragmentShortId={previewFragmentShortId}
247
+ descriptionFragmentShortId={descriptionFragmentShortId}
248
+ isLoadingDetails={isLoading}
249
+ />
250
+ );
251
+ }}
252
+ </DocumentsGetByIdClient>
253
+ )}
254
+ </div>
255
+ );
256
+ };
@@ -1,20 +1,8 @@
1
+ "use client";
2
+
1
3
  import { FC } from "react";
2
4
  import { CommonItemsModel } from "@c-rex/interfaces";
3
- import { FileStack } from "lucide-react";
4
- import { cn, findRelatedFragmentShortId, generateQueryParams } from "@c-rex/utils";
5
- import { Button } from "@c-rex/ui/button";
6
- import { Badge } from "@c-rex/ui/badge";
7
- import { Tooltip, TooltipContent, TooltipTrigger } from "@c-rex/ui/tooltip";
8
- import { RESULT_TYPES } from "@c-rex/constants";
9
- import { FileDownloadDropdown } from "@c-rex/components/file-download";
10
- import { FavoriteButton } from "@c-rex/components/favorite-button";
11
- import { ImageRenditionContainer } from "../renditions/image/container";
12
- import { BookmarkButton } from "../favorites/bookmark-button";
13
- import { HtmlRendition } from "../renditions/html";
14
- import { QueryParams } from "@c-rex/types";
15
- import { getResultItemSummary } from "../results/summary";
16
- import * as cheerio from "cheerio";
17
- import { ExpandableSummary } from "./expandable-summary";
5
+ import { DocumentsResultListItem } from "./result-list-item";
18
6
 
19
7
  interface Props {
20
8
  items: CommonItemsModel[];
@@ -22,6 +10,7 @@ interface Props {
22
10
  imageFragmentSubjectIds?: string[];
23
11
  descriptionFragmentSubjectIds?: string[];
24
12
  showActions?: boolean;
13
+ initialVisibleCount?: number;
25
14
  }
26
15
 
27
16
 
@@ -30,129 +19,21 @@ export const DocumentsResultList: FC<Props> = ({
30
19
  query = "",
31
20
  imageFragmentSubjectIds = [],
32
21
  descriptionFragmentSubjectIds = [],
22
+ initialVisibleCount = 4,
33
23
  }) => {
34
24
  return (
35
25
  <div className="flex-1">
36
26
  {items.map((item, index) => {
37
- const { title, language, itemType, multipleVersions, packageId } = getResultItemSummary(item);
38
- const isDocument = itemType === RESULT_TYPES.DOCUMENT;
39
- const previewFragmentShortId = findRelatedFragmentShortId({
40
- item,
41
- informationSubjectIds: imageFragmentSubjectIds,
42
- });
43
- const descriptionFragmentShortId = findRelatedFragmentShortId({
44
- item,
45
- informationSubjectIds: descriptionFragmentSubjectIds,
46
- });
47
- const queryParams: QueryParams[] = []
48
-
49
- if (packageId) {
50
- queryParams.push({
51
- key: "package",
52
- value: packageId,
53
- })
54
- }
55
-
56
- if (query.length > 0) {
57
- queryParams.push({
58
- key: "q",
59
- value: query,
60
- })
61
- }
62
-
63
- const params = generateQueryParams(queryParams)
64
- const itemLink = `/documents/${item.shortId}` + (params.length > 0 ? `?${params}` : "")
65
-
66
27
  return (
67
- <div
28
+ <DocumentsResultListItem
68
29
  key={item.shortId}
69
- className={cn(
70
- "min-h-12 flex flex-wrap items-start border px-4 py-2 gap-2 rounded",
71
- index == items.length - 1 ? "" : "mb-4",
72
- `c-rex-result-item c-rex-result-${itemType}`
73
- )}
74
- >
75
- <div className="h-16 w-16 flex shrink-0 items-start pt-1">
76
- <ImageRenditionContainer
77
- fragmentShortId={previewFragmentShortId}
78
- emptyImageStyle="h-16 w-16"
79
- />
80
- </div>
81
-
82
- <div className="flex-1 p-2 flex flex-col justify-start">
83
- <span className="text-sm text-muted-foreground">
84
- {item.revision} - {language}
85
- </span>
86
-
87
- <span className="text-lg font-medium">
88
- <a className="hover:underline" href={itemLink}>{title}</a>
89
- </span>
90
-
91
- <div>
92
- <Badge>{itemType}</Badge>
93
- </div>
94
- <span className="text-sm block">
95
- <HtmlRendition
96
- fragmentShortId={descriptionFragmentShortId}
97
- render={(html) => {
98
- const $ = cheerio.load(html);
99
- const rawText = ($("body").text() || "").replace(/\s+/g, " ").trim();
100
- const withoutCdata = rawText
101
- .replace(/\/\/<!\[CDATA\[/g, "")
102
- .replace(/\/\/\]\]>/g, "")
103
- .replace(/<!\[CDATA\[/g, "")
104
- .replace(/\]\]>/g, "")
105
- .trim();
106
- const withoutRepeatedTitle = withoutCdata.startsWith(title)
107
- ? withoutCdata.slice(title.length).trim()
108
- : withoutCdata;
109
- const descriptionText = withoutRepeatedTitle
110
- .replace(/^\/{2,}\s*/, "")
111
- .trim();
112
- if (!descriptionText) {
113
- return <span className="text-muted-foreground">No rendition available</span>;
114
- }
115
-
116
- return (
117
- <ExpandableSummary text={descriptionText} />
118
- );
119
- }}
120
- />
121
- </span>
122
- </div>
123
-
124
- <div className="flex flex-col p-2 ml-auto justify-start self-start">
125
- <div className="flex gap-2">
126
- <FileDownloadDropdown renditions={item.renditions} />
127
-
128
- <FavoriteButton
129
- id={item.shortId!}
130
- type={itemType}
131
- label={title}
132
- />
133
-
134
- {multipleVersions.length > 1 && (
135
- <Tooltip>
136
- <TooltipTrigger asChild>
137
- <Button variant="ghost" size="icon">
138
- <FileStack />
139
- </Button>
140
- </TooltipTrigger>
141
- <TooltipContent>
142
- Available in: {multipleVersions.join(", ")}
143
- </TooltipContent>
144
- </Tooltip>
145
- )}
146
-
147
- {isDocument && (
148
- <BookmarkButton
149
- shortId={item.shortId!}
150
- triggerVariant="ghost"
151
- />
152
- )}
153
- </div>
154
- </div>
155
- </div>
30
+ item={item}
31
+ query={query}
32
+ imageFragmentSubjectIds={imageFragmentSubjectIds}
33
+ descriptionFragmentSubjectIds={descriptionFragmentSubjectIds}
34
+ isLast={index === items.length - 1}
35
+ eager={index < initialVisibleCount}
36
+ />
156
37
  );
157
38
  })}
158
39
  </div>
@@ -0,0 +1,28 @@
1
+ import type { CommonItemsModel } from "@c-rex/interfaces";
2
+ import { findRelatedFragmentShortId } from "@c-rex/utils";
3
+
4
+ type RelatedInformationUnitLike =
5
+ | CommonItemsModel
6
+ | { informationUnits?: CommonItemsModel["informationUnits"] }
7
+ | null
8
+ | undefined;
9
+
10
+ export const resolveInformationUnitPreviewFragmentShortId = (
11
+ item: RelatedInformationUnitLike,
12
+ informationSubjectIds: string[] = []
13
+ ): string | undefined => {
14
+ return findRelatedFragmentShortId({
15
+ item,
16
+ informationSubjectIds,
17
+ });
18
+ };
19
+
20
+ export const resolveInformationUnitDescriptionFragmentShortId = (
21
+ item: RelatedInformationUnitLike,
22
+ informationSubjectIds: string[] = []
23
+ ): string | undefined => {
24
+ return findRelatedFragmentShortId({
25
+ item,
26
+ informationSubjectIds,
27
+ });
28
+ };