@c-rex/components 0.3.0-build.29 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-rex/components",
3
- "version": "0.3.0-build.29",
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"
@@ -196,7 +204,27 @@
196
204
  "./footer": {
197
205
  "types": "./src/footer/footer.tsx",
198
206
  "import": "./src/footer/footer.tsx"
199
- }
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
+ }
200
228
  },
201
229
  "scripts": {
202
230
  "storybook": "storybook dev -p 6006",
@@ -9,31 +9,30 @@ import {
9
9
  createContext,
10
10
  useContext,
11
11
  } from "react";
12
- import { cn, findRelatedFragmentShortId, formatDateToLocale, getLanguage, getTitle, getType } from "@c-rex/utils";
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 { useLocale, useMessages, useTranslations } from "next-intl";
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,7 +53,7 @@ type Props = {
54
53
  imageFragmentSubjectIds?: string[];
55
54
  }>;
56
55
  serviceType: keyof typeof ServiceOptions;
57
- queryParams?: Record<string, any>;
56
+ queryParams?: CarouselQueryParams;
58
57
  loadByPages?: boolean;
59
58
  linkPattern: string;
60
59
  imageFragmentSubjectIds?: string[];
@@ -305,8 +304,11 @@ export const Carousel: FC<Props> = ({
305
304
  </div>
306
305
  )}
307
306
  <div
308
- className="flex will-change-transform transition-all duration-600 ease-[cubic-bezier(0.4,0,0.2,1)] w-full"
309
- style={{ transform: `translateX(-${(current * 100) / slidesToShow}%)` }}
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
+ }}
310
312
  >
311
313
  {loading && (!data || data.length === 0) ? (
312
314
  <CarouselSkeletonTrack slidesToShow={slidesToShow} />
@@ -463,63 +465,4 @@ const DefaultRenderCarouselItem: FC<{
463
465
  loadImage: boolean;
464
466
  linkPattern: string;
465
467
  imageFragmentSubjectIds?: string[];
466
- }> = ({ item, showImages, loadImage, linkPattern, imageFragmentSubjectIds = [] }) => {
467
- const locale = useLocale();
468
- const messages = useMessages() as Record<string, unknown>;
469
- const t = useTranslations("itemTypes");
470
-
471
- const date = formatDateToLocale(item.created!, locale);
472
- const title = getTitle(item.titles, item.labels);
473
- const itemType = getType(item.class, locale);
474
- const itemTypeKey = itemType?.toLowerCase();
475
- const itemTypeMessages = (messages.itemTypes || {}) as Record<string, string>;
476
- const itemTypeLabel = itemTypeKey && itemTypeMessages[itemTypeKey]
477
- ? t(itemTypeKey)
478
- : itemType;
479
- const language = getLanguage(item.languages)
480
- const countryCode = language.split("-")[1] || "";
481
- const link = linkPattern.replace("{shortId}", item.shortId!);
482
- const previewFragmentShortId = findRelatedFragmentShortId({
483
- item,
484
- informationSubjectIds: imageFragmentSubjectIds,
485
- });
486
-
487
- return (
488
- <Link href={link} className="group p-2 flex flex-1">
489
- <Card className={cn("p-4 flex-1 justify-between relative", !showImages && "min-h-0")}>
490
- {itemType && (
491
- <Badge className="absolute -top-2 -right-2">{itemTypeLabel}</Badge>
492
- )}
493
-
494
- {showImages ? (
495
- <div className="h-48 w-full overflow-hidden">
496
- <ImageRenditionContainer
497
- fragmentShortId={loadImage ? previewFragmentShortId : undefined}
498
- emptyImageStyle="h-48 w-full"
499
- skeletonStyle="h-48 w-full"
500
- imageStyle="object-contain object-top h-48 !w-auto max-w-full mx-auto"
501
- />
502
- </div>
503
- ) : null}
504
-
505
- <Tooltip>
506
- <TooltipTrigger asChild>
507
- <div className="h-44 overflow-hidden">
508
- <span className="line-clamp-5 text-lg font-semibold group-hover:underline">
509
- {title}
510
- </span>
511
- </div>
512
- </TooltipTrigger>
513
- <TooltipContent className="max-w-sm break-words">{title}</TooltipContent>
514
- </Tooltip>
515
-
516
- <div className="flex justify-between w-full">
517
- <span className="w-8 block">
518
- <Flag countryCode={countryCode} />
519
- </span>
520
- <span className="text-gray-400">{date || item.revision}</span>
521
- </div>
522
- </Card>
523
- </Link>
524
- );
525
- };
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,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
+ };
@@ -0,0 +1,368 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { useLocale, useTranslations } from "next-intl";
5
+ import Link from "next/link";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@c-rex/ui/card";
7
+ import { Table, TableBody, TableCell, TableRow } from "@c-rex/ui/table";
8
+ import { Flag } from "../icons/flag-icon";
9
+ import { BookmarkButton } from "../favorites/bookmark-button";
10
+ import { renderMetadataDisplayValues } from "./shared";
11
+ import { InformationUnitsGetAllClient } from "../generated/client-components";
12
+ import type {
13
+ CommonItemsModel,
14
+ InformationUnitModel,
15
+ LiteralModel,
16
+ ObjectRefModel,
17
+ RenditionModel,
18
+ } from "@c-rex/interfaces";
19
+ import {
20
+ INFORMATION_UNIT_PROPERTY_PRESENTATION,
21
+ type InformationUnitPropertyKey,
22
+ } from "@c-rex/services/metadata-presentation-config";
23
+ import { resolveMetadataDisplayProperties } from "@c-rex/services/metadata-view-profile";
24
+ import {
25
+ extractCountryCodeFromLanguage,
26
+ getFileRenditionGroups,
27
+ resolvePreferredLanguage,
28
+ sortAndDeduplicateLanguages,
29
+ } from "@c-rex/utils";
30
+
31
+ type MetadataDisplayRow = {
32
+ key: InformationUnitPropertyKey;
33
+ label: string;
34
+ labelSource: "translationKey" | "direct";
35
+ values: string[];
36
+ valueType?: "text" | "language" | "rendition";
37
+ renditions?: RenditionModel[];
38
+ };
39
+
40
+ type Props = {
41
+ title: string;
42
+ linkPattern: string;
43
+ data: CommonItemsModel;
44
+ metadataIncludeProperties?: Array<keyof InformationUnitModel>;
45
+ metadataExcludeProperties?: Array<keyof InformationUnitModel>;
46
+ showBookmarkButton?: boolean;
47
+ showFileRenditions?: boolean;
48
+ embedded?: boolean;
49
+ };
50
+
51
+ const resolveLiteralLabel = (labels?: LiteralModel[] | null, uiLanguage?: string): string | undefined => {
52
+ if (!labels || labels.length === 0) return undefined;
53
+ const language = uiLanguage || "en";
54
+ const preferred = resolvePreferredLanguage(
55
+ labels.map((item) => item.language || ""),
56
+ language
57
+ );
58
+ if (preferred) {
59
+ const exact = labels.find((item) => item.language === preferred)?.value;
60
+ if (exact) return exact;
61
+ }
62
+ return labels.find((item) => item.value)?.value || undefined;
63
+ };
64
+
65
+ const resolveObjectRefLabel = (item: ObjectRefModel, uiLanguage: string): string | undefined => {
66
+ return resolveLiteralLabel(item.labels || [], uiLanguage) || item.shortId || item.id || undefined;
67
+ };
68
+
69
+ const asObjectRefArray = (value: unknown): ObjectRefModel[] => {
70
+ if (!Array.isArray(value)) return [];
71
+ return value.filter((item): item is ObjectRefModel => Boolean(item && typeof item === "object"));
72
+ };
73
+
74
+ const asLiteralArray = (value: unknown): LiteralModel[] => {
75
+ if (!Array.isArray(value)) return [];
76
+ return value.filter((item): item is LiteralModel => Boolean(item && typeof item === "object"));
77
+ };
78
+
79
+ const buildMetadataDisplayRows = (
80
+ data: CommonItemsModel,
81
+ uiLanguage: string,
82
+ includeProperties: InformationUnitPropertyKey[]
83
+ ): MetadataDisplayRow[] => {
84
+ const preferredTitle = resolveLiteralLabel(data.titles || [], uiLanguage) || resolveLiteralLabel(data.labels || [], uiLanguage);
85
+ const rows: MetadataDisplayRow[] = [];
86
+
87
+ for (const key of includeProperties) {
88
+ const config = INFORMATION_UNIT_PROPERTY_PRESENTATION[key];
89
+ if (!config?.metadataDisplay?.supported) continue;
90
+
91
+ if (key === "labels") continue;
92
+
93
+ if (key === "titles") {
94
+ if (preferredTitle) {
95
+ rows.push({
96
+ key,
97
+ label: key,
98
+ labelSource: "translationKey",
99
+ values: [preferredTitle],
100
+ valueType: "text",
101
+ });
102
+ }
103
+ continue;
104
+ }
105
+
106
+ const value = (data as InformationUnitModel)[key];
107
+ if (value == null) continue;
108
+
109
+ if (key === "languages") {
110
+ const languages = sortAndDeduplicateLanguages((value as string[]) || []);
111
+ if (languages.length > 0) {
112
+ rows.push({
113
+ key,
114
+ label: key,
115
+ labelSource: "translationKey",
116
+ values: languages,
117
+ valueType: "language",
118
+ });
119
+ }
120
+ continue;
121
+ }
122
+
123
+ if (config.valueKind === "scalar") {
124
+ rows.push({
125
+ key,
126
+ label: key,
127
+ labelSource: "translationKey",
128
+ values: [String(value)],
129
+ valueType: "text",
130
+ });
131
+ continue;
132
+ }
133
+
134
+ if (config.valueKind === "stringArray") {
135
+ const values = Array.from(new Set((value as string[]).map((item) => String(item)).filter(Boolean)));
136
+ if (values.length > 0) {
137
+ rows.push({
138
+ key,
139
+ label: key,
140
+ labelSource: "translationKey",
141
+ values,
142
+ valueType: "text",
143
+ });
144
+ }
145
+ continue;
146
+ }
147
+
148
+ if (config.valueKind === "literalArray") {
149
+ const preferred = resolveLiteralLabel(asLiteralArray(value), uiLanguage);
150
+ if (preferred) {
151
+ rows.push({
152
+ key,
153
+ label: key,
154
+ labelSource: "translationKey",
155
+ values: [preferred],
156
+ valueType: "text",
157
+ });
158
+ }
159
+ continue;
160
+ }
161
+
162
+ if (config.valueKind === "objectRef") {
163
+ const label = resolveObjectRefLabel(value as ObjectRefModel, uiLanguage);
164
+ if (label) {
165
+ rows.push({
166
+ key,
167
+ label: key,
168
+ labelSource: "translationKey",
169
+ values: [label],
170
+ valueType: "text",
171
+ });
172
+ }
173
+ continue;
174
+ }
175
+
176
+ if (config.valueKind === "objectRefArray") {
177
+ const refs = asObjectRefArray(value);
178
+ if (refs.length === 0) continue;
179
+
180
+ const values = Array.from(
181
+ new Set(
182
+ refs
183
+ .map((ref) => resolveObjectRefLabel(ref, uiLanguage))
184
+ .filter((label): label is string => Boolean(label))
185
+ )
186
+ ).sort((a, b) => a.localeCompare(b));
187
+
188
+ if (values.length === 0) continue;
189
+
190
+ rows.push({
191
+ key,
192
+ label: key,
193
+ labelSource: "translationKey",
194
+ values,
195
+ valueType: "text",
196
+ });
197
+ continue;
198
+ }
199
+
200
+ if (config.valueKind === "renditionArray") {
201
+ const renditions = Array.isArray(value)
202
+ ? value.filter((item): item is RenditionModel => Boolean(item && typeof item === "object"))
203
+ : [];
204
+ if (renditions.length === 0) continue;
205
+ if (getFileRenditionGroups({ renditions }).length === 0) continue;
206
+
207
+ rows.push({
208
+ key,
209
+ label: "files",
210
+ labelSource: "translationKey",
211
+ values: [],
212
+ valueType: "rendition",
213
+ renditions,
214
+ });
215
+ }
216
+ }
217
+
218
+ return rows;
219
+ };
220
+
221
+ const normalizeVersionItems = (items: CommonItemsModel[], uiLanguage: string) => {
222
+ const uniqueByShortId = new Map<string, { shortId: string; language: string }>();
223
+
224
+ items.forEach((item) => {
225
+ const shortId = item.shortId?.trim();
226
+ if (!shortId) return;
227
+ const language = resolvePreferredLanguage(item.languages, uiLanguage);
228
+ if (!language) return;
229
+ uniqueByShortId.set(shortId, { shortId, language });
230
+ });
231
+
232
+ return Array.from(uniqueByShortId.values()).sort((a, b) => {
233
+ const languageOrder = a.language.localeCompare(b.language);
234
+ if (languageOrder !== 0) return languageOrder;
235
+ return a.shortId.localeCompare(b.shortId);
236
+ });
237
+ };
238
+
239
+ const AvailableInRow = ({
240
+ versionOfShortId,
241
+ currentShortId,
242
+ linkPattern,
243
+ }: {
244
+ versionOfShortId?: string | null;
245
+ currentShortId?: string | null;
246
+ linkPattern: string;
247
+ }) => {
248
+ const t = useTranslations();
249
+ const locale = useLocale();
250
+
251
+ if (!versionOfShortId) return null;
252
+
253
+ return (
254
+ <InformationUnitsGetAllClient
255
+ queryParams={{
256
+ Restrict: [`versionOf.shortId=${versionOfShortId}`],
257
+ Fields: ["shortId", "languages", "labels", "versionOf"],
258
+ PageNumber: 1,
259
+ PageSize: 200,
260
+ Sort: ["languages"],
261
+ }}
262
+ >
263
+ {({ data }) => {
264
+ const versions = normalizeVersionItems(data?.items || [], locale)
265
+ .filter((item) => item.shortId !== currentShortId);
266
+
267
+ if (versions.length === 0) return null;
268
+
269
+ return (
270
+ <TableRow className="min-h-12">
271
+ <TableCell className="font-medium w-28 pl-4">
272
+ <h4 className="text-sm font-medium">{t("availableIn")}</h4>
273
+ </TableCell>
274
+ <TableCell className="text-xs text-muted-foreground flex items-center gap-2 min-h-12">
275
+ {versions.map((item) => (
276
+ <span className="w-8 block border" key={item.shortId}>
277
+ <Link
278
+ href={linkPattern.replace("{shortId}", item.shortId)}
279
+ title={item.language}
280
+ >
281
+ <Flag countryCode={extractCountryCodeFromLanguage(item.language)} />
282
+ </Link>
283
+ </span>
284
+ ))}
285
+ </TableCell>
286
+ </TableRow>
287
+ );
288
+ }}
289
+ </InformationUnitsGetAllClient>
290
+ );
291
+ };
292
+
293
+ export const InformationUnitMetadataGridClient = ({
294
+ title,
295
+ data,
296
+ embedded = false,
297
+ linkPattern,
298
+ metadataIncludeProperties,
299
+ metadataExcludeProperties,
300
+ showBookmarkButton = false,
301
+ showFileRenditions = true,
302
+ }: Props) => {
303
+ const t = useTranslations();
304
+ const locale = useLocale();
305
+
306
+ const metadataRows = useMemo(() => {
307
+ const includeProperties = resolveMetadataDisplayProperties({
308
+ includeProperties: metadataIncludeProperties as string[] | undefined,
309
+ excludeProperties: showFileRenditions
310
+ ? (metadataExcludeProperties as string[] | undefined)
311
+ : [...(metadataExcludeProperties || []), "renditions"],
312
+ });
313
+
314
+ return buildMetadataDisplayRows(data, locale, includeProperties);
315
+ }, [data, locale, metadataExcludeProperties, metadataIncludeProperties, showFileRenditions]);
316
+
317
+ const cardContent = (
318
+ <CardContent className="space-y-3 !p-0">
319
+ <Table className="table-fixed">
320
+ <TableBody>
321
+ {showBookmarkButton && (
322
+ <TableRow className="min-h-12">
323
+ <TableCell className="font-medium w-32 pl-4 align-top">
324
+ <h4 className="text-sm font-medium">{t("favorites")}</h4>
325
+ </TableCell>
326
+ <TableCell className="min-h-12 pt-3 text-xs text-muted-foreground align-top break-words whitespace-normal">
327
+ <BookmarkButton shortId={data.shortId!} />
328
+ </TableCell>
329
+ </TableRow>
330
+ )}
331
+
332
+ {metadataRows.map((row) => (
333
+ <TableRow key={`${row.key}:${row.label}`} className="min-h-12">
334
+ <TableCell className="font-medium w-32 pl-4 align-top">
335
+ <h4 className="text-sm font-medium capitalize">
336
+ {row.labelSource === "translationKey" ? t(row.label) : row.label}
337
+ </h4>
338
+ </TableCell>
339
+ <TableCell className="min-h-12 text-xs text-muted-foreground align-top break-words whitespace-normal [&_*]:break-words">
340
+ {renderMetadataDisplayValues(row, locale)}
341
+ </TableCell>
342
+ </TableRow>
343
+ ))}
344
+
345
+ <AvailableInRow
346
+ versionOfShortId={data.versionOf?.shortId}
347
+ currentShortId={data.shortId}
348
+ linkPattern={linkPattern}
349
+ />
350
+ </TableBody>
351
+ </Table>
352
+ </CardContent>
353
+ );
354
+
355
+ if (embedded) return cardContent;
356
+
357
+ return (
358
+ <Card className="!p-0 !gap-0">
359
+ <CardHeader>
360
+ <CardTitle className="text-lg flex justify-between items-end">
361
+ {title}
362
+ {showBookmarkButton && <BookmarkButton shortId={data.shortId!} />}
363
+ </CardTitle>
364
+ </CardHeader>
365
+ {cardContent}
366
+ </Card>
367
+ );
368
+ };