@c-rex/components 0.3.0-build.35 → 0.3.0-build.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +32 -36
  2. package/src/article/article-action-bar.tsx +12 -2
  3. package/src/{check-article-lang.tsx → article/check-article-lang.tsx} +1 -1
  4. package/src/article/render-article-highlight.tsx +108 -0
  5. package/src/article/render-article.tsx +28 -0
  6. package/src/autocomplete.tsx +7 -25
  7. package/src/blog/blog-author-card.tsx +116 -0
  8. package/src/carousel/carousel.tsx +5 -2
  9. package/src/carousel/information-unit-carousel-item.tsx +1 -1
  10. package/src/content-unavailable.tsx +20 -0
  11. package/src/directoryNodes/directory-tree-context.tsx +9 -4
  12. package/src/documents/description-preview.tsx +14 -4
  13. package/src/documents/result-list-item.tsx +40 -46
  14. package/src/favorites/__tests__/favorites-hydration.test.tsx +245 -0
  15. package/src/favorites/bookmark-button.tsx +38 -20
  16. package/src/favorites/favorite-button.tsx +23 -24
  17. package/src/favorites/favorites-context.tsx +287 -0
  18. package/src/icons/file-icon.tsx +9 -26
  19. package/src/info/information-unit-metadata-grid-client.tsx +21 -21
  20. package/src/navbar/navbar.tsx +16 -30
  21. package/src/navbar/settings.tsx +1 -1
  22. package/src/page-wrapper.tsx +3 -3
  23. package/src/renditions/html-client.tsx +8 -6
  24. package/src/renditions/html.tsx +3 -1
  25. package/src/restriction-menu/restriction-menu-item.tsx +48 -58
  26. package/src/restriction-menu/restriction-selection-command-menu.tsx +445 -0
  27. package/src/restriction-menu/restriction-selection-menu.tsx +5 -7
  28. package/src/restriction-menu/taxonomy-restriction-command-menu.tsx +111 -0
  29. package/src/restriction-menu/taxonomy-restriction-menu.tsx +19 -12
  30. package/src/results/filter-navbar.tsx +81 -76
  31. package/src/results/filter-sidebar/context.tsx +32 -0
  32. package/src/results/filter-sidebar/index.tsx +40 -35
  33. package/src/results/generic/search-results-client.tsx +5 -4
  34. package/src/results/generic/table-result-list.tsx +16 -16
  35. package/src/results/information-unit-search-results-card-list.tsx +4 -1
  36. package/src/results/information-unit-search-results-cards.tsx +169 -69
  37. package/src/results/pagination.tsx +43 -40
  38. package/src/search-input.tsx +4 -2
  39. package/src/toc/toc-breadcrumb.tsx +1 -1
  40. package/src/toc/toc-browse-controls.tsx +2 -2
  41. package/src/toc/toc-tree-panel.tsx +19 -16
  42. package/src/article/article-content.tsx +0 -19
  43. package/src/breadcrumb.tsx +0 -124
  44. package/src/directoryNodes/tree-of-content.tsx +0 -68
  45. package/src/render-article.tsx +0 -75
  46. package/src/restriction-menu/restriction-menu-container.tsx +0 -4
  47. package/src/restriction-menu/restriction-menu.tsx +0 -4
  48. package/src/stores/__tests__/favorites-store.test.ts +0 -54
  49. package/src/stores/favorites-store.ts +0 -163
  50. /package/src/{render-article.module.css → article/render-article.module.css} +0 -0
@@ -0,0 +1,287 @@
1
+ "use client";
2
+
3
+ import { Favorite } from "@c-rex/types";
4
+ import { getLocalStorageJson, removeLocalStorageItem, setLocalStorageJson } from "@c-rex/utils";
5
+ import { ReactNode, createContext, useContext, useEffect, useState } from "react";
6
+
7
+ type FavoriteDocumentState = { topics: Favorite[]; label?: string };
8
+ type FavoriteDocumentsMap = Record<string, FavoriteDocumentState>;
9
+
10
+ export type FavoritesState = {
11
+ favorites: Favorite[];
12
+ documents: FavoriteDocumentsMap;
13
+ };
14
+
15
+ export type FavoritesActions = {
16
+ favoriteTopic: (documentId: string, id: string, label: string, color: string) => void;
17
+ unfavoriteTopic: (documentId: string, id: string) => void;
18
+ favoriteDocument: (id: string, label: string) => void;
19
+ unfavoriteDocument: (id: string) => void;
20
+ };
21
+
22
+ export type FavoritesContextValue = FavoritesState & FavoritesActions & {
23
+ isHydrated: boolean;
24
+ };
25
+
26
+ type PersistedFavoritesState = FavoritesState | {
27
+ state?: FavoritesState;
28
+ version?: number;
29
+ };
30
+
31
+ export const FAVORITES_STORAGE_KEY = "c-rex-favorites";
32
+
33
+ export const defaultFavoritesState: FavoritesState = {
34
+ documents: {},
35
+ favorites: [],
36
+ };
37
+
38
+ export const loadFavoritesState = (fallbackState: FavoritesState = defaultFavoritesState): FavoritesState => {
39
+ const persisted = getLocalStorageJson<PersistedFavoritesState>(FAVORITES_STORAGE_KEY);
40
+
41
+ if (!persisted) {
42
+ return fallbackState;
43
+ }
44
+
45
+ if (!isFavoritesState(persisted)) {
46
+ return persisted.state ?? fallbackState;
47
+ }
48
+
49
+ return persisted;
50
+ };
51
+
52
+ export const persistFavoritesState = (state: FavoritesState): void => {
53
+ setLocalStorageJson(FAVORITES_STORAGE_KEY, state);
54
+ };
55
+
56
+ export const clearFavoritesState = (): void => {
57
+ removeLocalStorageItem(FAVORITES_STORAGE_KEY);
58
+ };
59
+
60
+ export const applyFavoriteTopic = (state: FavoritesState, documentId: string, id: string, label: string, color: string): FavoritesState => {
61
+ const documents = favoriteTopic(state.documents, documentId, id, label, color);
62
+ const favorites = upsertFavorites(state.favorites, [
63
+ { id, label, color },
64
+ { id: documentId, label: state.documents[documentId]?.label || "", color: "" },
65
+ ]);
66
+
67
+ if (documents === state.documents && favorites === state.favorites) {
68
+ return state;
69
+ }
70
+
71
+ return { ...state, documents, favorites };
72
+ };
73
+
74
+ export const applyUnfavoriteTopic = (state: FavoritesState, documentId: string, id: string): FavoritesState => ({
75
+ ...state,
76
+ documents: unfavoriteTopic(state.documents, documentId, id),
77
+ favorites: state.favorites.filter((topic) => topic.id !== id),
78
+ });
79
+
80
+ export const applyFavoriteDocument = (state: FavoritesState, id: string, label: string): FavoritesState => {
81
+ const documents = upsertDocument(state.documents, id, label);
82
+ const favorites = upsertFavorites(state.favorites, [{ id, label, color: "" }]);
83
+
84
+ if (documents === state.documents && favorites === state.favorites) {
85
+ return state;
86
+ }
87
+
88
+ return {
89
+ ...state,
90
+ documents,
91
+ favorites,
92
+ };
93
+ };
94
+
95
+ export const applyUnfavoriteDocument = (state: FavoritesState, id: string): FavoritesState => {
96
+ const documentsCopy = { ...state.documents };
97
+ if (!documentsCopy[id]) {
98
+ return state;
99
+ }
100
+
101
+ const favoritesToRemove = documentsCopy[id]?.topics.map((topic) => topic.id) || [];
102
+ const newFavorites = state.favorites.filter((fav) => fav.id !== id && !favoritesToRemove.includes(fav.id));
103
+ delete documentsCopy[id];
104
+
105
+ return {
106
+ ...state,
107
+ documents: documentsCopy,
108
+ favorites: newFavorites,
109
+ };
110
+ };
111
+
112
+ const FavoritesContext = createContext<FavoritesContextValue | undefined>(undefined);
113
+
114
+ type FavoritesProviderProps = {
115
+ children: ReactNode;
116
+ initialState?: FavoritesState;
117
+ };
118
+
119
+ export const FavoritesProvider = ({
120
+ children,
121
+ initialState = defaultFavoritesState,
122
+ }: FavoritesProviderProps) => {
123
+ const [state, setState] = useState<FavoritesState>(initialState);
124
+ const [isHydrated, setIsHydrated] = useState(false);
125
+
126
+ useEffect(() => {
127
+ setState(loadFavoritesState(initialState));
128
+ setIsHydrated(true);
129
+
130
+ const handleStorageChange = () => {
131
+ setState(loadFavoritesState(initialState));
132
+ };
133
+
134
+ window.addEventListener("storage", handleStorageChange);
135
+ return () => window.removeEventListener("storage", handleStorageChange);
136
+ }, [initialState]);
137
+
138
+ const favoriteTopic = (documentId: string, id: string, label: string, color: string) => {
139
+ setState((currentState) => {
140
+ const nextState = applyFavoriteTopic(currentState, documentId, id, label, color);
141
+ persistFavoritesState(nextState);
142
+ return nextState;
143
+ });
144
+ };
145
+
146
+ const unfavoriteTopic = (documentId: string, id: string) => {
147
+ setState((currentState) => {
148
+ const nextState = applyUnfavoriteTopic(currentState, documentId, id);
149
+ persistFavoritesState(nextState);
150
+ return nextState;
151
+ });
152
+ };
153
+
154
+ const favoriteDocument = (id: string, label: string) => {
155
+ setState((currentState) => {
156
+ const nextState = applyFavoriteDocument(currentState, id, label);
157
+ persistFavoritesState(nextState);
158
+ return nextState;
159
+ });
160
+ };
161
+
162
+ const unfavoriteDocument = (id: string) => {
163
+ setState((currentState) => {
164
+ const nextState = applyUnfavoriteDocument(currentState, id);
165
+ persistFavoritesState(nextState);
166
+ return nextState;
167
+ });
168
+ };
169
+
170
+ const contextValue: FavoritesContextValue = {
171
+ ...state,
172
+ isHydrated,
173
+ favoriteTopic,
174
+ unfavoriteTopic,
175
+ favoriteDocument,
176
+ unfavoriteDocument,
177
+ };
178
+
179
+ return (
180
+ <FavoritesContext.Provider value={contextValue}>
181
+ {children}
182
+ </FavoritesContext.Provider>
183
+ );
184
+ };
185
+
186
+ export const useFavorites = <T,>(selector: (state: FavoritesContextValue) => T): T => {
187
+ const favoritesContext = useContext(FavoritesContext);
188
+
189
+ if (!favoritesContext) {
190
+ throw new Error("useFavorites must be used within FavoritesProvider");
191
+ }
192
+
193
+ return selector(favoritesContext);
194
+ };
195
+
196
+ const favoriteTopic = (documents: FavoriteDocumentsMap, documentId: string, id: string, label: string, color: string): FavoriteDocumentsMap => {
197
+ const currentDocument = documents[documentId];
198
+ const currentTopics = currentDocument?.topics ?? [];
199
+ if (currentTopics.some((topic) => topic.id === id)) {
200
+ return documents;
201
+ }
202
+
203
+ return {
204
+ ...documents,
205
+ [documentId]: {
206
+ ...currentDocument,
207
+ topics: [...currentTopics, { id, label, color }],
208
+ },
209
+ };
210
+ };
211
+
212
+ const unfavoriteTopic = (documents: FavoriteDocumentsMap, documentId: string, id: string): FavoriteDocumentsMap => {
213
+ const currentDocument = documents[documentId];
214
+ if (!currentDocument) {
215
+ return documents;
216
+ }
217
+
218
+ return {
219
+ ...documents,
220
+ [documentId]: {
221
+ ...currentDocument,
222
+ topics: currentDocument.topics.filter((topic) => topic.id !== id),
223
+ },
224
+ };
225
+ };
226
+
227
+ const upsertDocument = (documents: FavoriteDocumentsMap, documentId: string, label: string): FavoriteDocumentsMap => {
228
+ const currentDocument = documents[documentId];
229
+
230
+ if (!currentDocument) {
231
+ return {
232
+ ...documents,
233
+ [documentId]: { topics: [], label },
234
+ };
235
+ }
236
+
237
+ if (currentDocument.label === label) {
238
+ return documents;
239
+ }
240
+
241
+ return {
242
+ ...documents,
243
+ [documentId]: {
244
+ ...currentDocument,
245
+ label,
246
+ },
247
+ };
248
+ };
249
+
250
+ const upsertFavorites = (favorites: Favorite[], entries: Favorite[]): Favorite[] => {
251
+ let nextFavorites = favorites;
252
+
253
+ for (const entry of entries) {
254
+ nextFavorites = upsertFavorite(nextFavorites, entry);
255
+ }
256
+
257
+ return nextFavorites;
258
+ };
259
+
260
+ const upsertFavorite = (favorites: Favorite[], entry: Favorite): Favorite[] => {
261
+ const index = favorites.findIndex((favorite) => favorite.id === entry.id);
262
+ if (index === -1) {
263
+ return [...favorites, entry];
264
+ }
265
+
266
+ const current = favorites[index];
267
+ const next = {
268
+ ...current,
269
+ label: current.label || entry.label,
270
+ color: current.color || entry.color,
271
+ };
272
+
273
+ if (
274
+ current.label === next.label &&
275
+ current.color === next.color
276
+ ) {
277
+ return favorites;
278
+ }
279
+
280
+ const copy = [...favorites];
281
+ copy[index] = next;
282
+ return copy;
283
+ };
284
+
285
+ const isFavoritesState = (value: PersistedFavoritesState): value is FavoritesState => (
286
+ "favorites" in value && "documents" in value
287
+ );
@@ -34,34 +34,25 @@ const getGenericDisplayCode = (mimeType: string) => {
34
34
 
35
35
  const GenericFileTypeIcon = ({ code }: { code: string }) => (
36
36
  <svg
37
- viewBox="0 0 24 24"
37
+ strokeWidth="0"
38
+ viewBox="0 0 256 256"
38
39
  className="size-5 text-primary"
39
40
  aria-hidden="true"
40
- fill="none"
41
+ fill="currentColor"
41
42
  xmlns="http://www.w3.org/2000/svg"
42
43
  >
43
44
  <path
44
- d="M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-6Z"
45
- stroke="currentColor"
46
- strokeWidth="1.5"
47
- fill="currentColor"
48
- fillOpacity="0.08"
49
- />
50
- <path
51
- d="M14 2v6h5"
52
- stroke="currentColor"
53
- strokeWidth="1.5"
54
- strokeLinecap="round"
55
- strokeLinejoin="round"
45
+ d="M36,112V40A16,16,0,0,1,52,24h100a8,8,0,0,1,5.66,2.34l56,56A8,8,0,0,1,216,88v24a8,8,0,0,1-16,0V96H152a8,8,0,0,1-8-8V40H52v72a8,8,0,0,1-16,0ZM160,80h28.69L160,51.31Z"
56
46
  />
57
47
  <text
58
- x="12"
59
- y="17.25"
48
+ x="128"
49
+ y="220"
60
50
  textAnchor="middle"
61
- fontSize="5.2"
51
+ fontSize="110"
62
52
  fontWeight="700"
63
53
  fill="currentColor"
64
- letterSpacing="0.4"
54
+ fontFamily="inherit"
55
+ letterSpacing={code.length >= 4 ? -2 : 0}
65
56
  >
66
57
  {code}
67
58
  </text>
@@ -69,13 +60,5 @@ const GenericFileTypeIcon = ({ code }: { code: string }) => (
69
60
  );
70
61
 
71
62
  export const FileIcon = ({ extension }: { extension: string }) => {
72
- if (extension === "application/pdf") {
73
- return <PiFilePdf className="text-primary" />;
74
- }
75
-
76
- if (extension === "application/zip") {
77
- return <PiFileZip className="text-primary" />;
78
- }
79
-
80
63
  return <GenericFileTypeIcon code={getGenericDisplayCode(extension)} />;
81
64
  };
@@ -344,7 +344,7 @@ export const InformationUnitMetadataGridClient = ({
344
344
 
345
345
  const cardContent = (
346
346
  <CardContent className="space-y-3 !p-0">
347
- <Table className="table-fixed">
347
+ <Table className="table-fixed overflow-ellipsis">
348
348
  <TableBody>
349
349
  {showBookmarkButton && (
350
350
  <TableRow className="min-h-12">
@@ -374,26 +374,26 @@ export const InformationUnitMetadataGridClient = ({
374
374
  availableInVersions
375
375
  .filter((item) => item.shortId !== data.shortId)
376
376
  .length > 0 ? (
377
- <TableRow className="min-h-12">
378
- <TableCell className="font-medium w-28 pl-4">
379
- <h4 className="text-sm font-medium">{t("availableIn")}</h4>
380
- </TableCell>
381
- <TableCell className="text-xs text-muted-foreground flex items-center gap-2 min-h-12">
382
- {availableInVersions
383
- .filter((item) => item.shortId !== data.shortId)
384
- .map((item) => (
385
- <span className="w-8 block border" key={item.shortId}>
386
- <Link
387
- href={linkPattern.replace("{shortId}", item.shortId)}
388
- title={item.language}
389
- >
390
- <Flag countryCode={extractCountryCodeFromLanguage(item.language)} />
391
- </Link>
392
- </span>
393
- ))}
394
- </TableCell>
395
- </TableRow>
396
- ) : null
377
+ <TableRow className="min-h-12">
378
+ <TableCell className="font-medium w-28 pl-4">
379
+ <h4 className="text-sm font-medium">{t("availableIn")}</h4>
380
+ </TableCell>
381
+ <TableCell className="text-xs text-muted-foreground flex items-center gap-2 min-h-12">
382
+ {availableInVersions
383
+ .filter((item) => item.shortId !== data.shortId)
384
+ .map((item) => (
385
+ <span className="w-8 block border" key={item.shortId}>
386
+ <Link
387
+ href={linkPattern.replace("{shortId}", item.shortId)}
388
+ title={item.language}
389
+ >
390
+ <Flag countryCode={extractCountryCodeFromLanguage(item.language)} />
391
+ </Link>
392
+ </span>
393
+ ))}
394
+ </TableCell>
395
+ </TableRow>
396
+ ) : null
397
397
  ) : (
398
398
  <AvailableInRow
399
399
  versionOfShortId={data.versionOf?.shortId}
@@ -11,7 +11,7 @@ import { cn } from "@c-rex/utils";
11
11
  import { getTranslations } from "next-intl/server";
12
12
  import { Button } from "@c-rex/ui/button";
13
13
  import { DropdownHoverItem } from "@c-rex/ui/dropdown-hover-item";
14
- import { Menu } from "lucide-react";
14
+ import { Menu, Search } from "lucide-react";
15
15
  import { getOrganizationBranding } from "@c-rex/services/vcard";
16
16
 
17
17
  type NavBarProps = {
@@ -53,14 +53,9 @@ export const NavBar: FC<NavBarProps> = async ({
53
53
  }
54
54
 
55
55
  return (
56
- <header className="sticky flex flex-col top-0 z-40 w-full p-4 backdrop-blur-xl transition-all bg-transparent border-b gap-2">
57
- <div className="w-full flex items-center justify-between gap-2">
58
- <div
59
- className={cn(
60
- "flex items-center gap-4",
61
- title && "lg:w-[calc(16rem-16px)]"
62
- )}
63
- >
56
+ <header className="sticky flex flex-col top-0 z-40 w-full backdrop-blur-xl transition-all bg-transparent border-b">
57
+ <div className="w-full flex items-center justify-between gap-2 p-4">
58
+ <div className="flex items-center gap-4">
64
59
  {showMenu && (
65
60
  <DropdownHoverItem
66
61
  label={
@@ -107,28 +102,22 @@ export const NavBar: FC<NavBarProps> = async ({
107
102
  <img
108
103
  src={organizationBranding.logoSrc}
109
104
  alt={`${organizationBranding.organizationName} logo`}
110
- className="h-14"
105
+ className="h-10"
111
106
  />
112
107
  </Link>
113
108
  )}
114
109
  </div>
115
110
 
116
- {title && (
117
- <div className="flex-1 hidden md:flex md:justify-center lg:justify-start">
118
- <h1 className="md:text-2xl lg:text-3xl font-bold tracking-tight text-balance">{title}</h1>
119
- </div>
120
- )}
121
-
122
111
  <div className="flex gap-2">
123
- {willShowInput &&
124
- <div className="hidden sm:flex flex-1 items-center px-3 border rounded-full h-8 c-rex-search-bar">
125
- <SearchInput
126
- autocompleteType={autocompleteType}
127
- onSelectPath={onSelectPath}
128
- {...props}
129
- />
130
- </div>
131
- }
112
+
113
+
114
+ <Button rounded="full" size="sm" className="w-8" variant="ghost">
115
+ <Search className="!size-5" />
116
+ </Button>
117
+
118
+ {clientConfigs.languageSwitcher.enabled && (
119
+ <SettingsMenu />
120
+ )}
132
121
 
133
122
  {clientConfigs.OIDC.userEnabled && (
134
123
  <>
@@ -140,15 +129,12 @@ export const NavBar: FC<NavBarProps> = async ({
140
129
  </>
141
130
  )}
142
131
 
143
- {clientConfigs.languageSwitcher.enabled && (
144
- <SettingsMenu />
145
- )}
146
132
  </div>
147
133
  </div>
148
134
 
149
135
  {title && (
150
- <div className="flex-1 flex justify-center md:hidden">
151
- <h1 className="text-2xl font-bold tracking-tight text-balance">{title}</h1>
136
+ <div className="flex-1 flex justify-center border-t py-2">
137
+ <h1 className="text-2xl font-bold tracking-tight text-balance text-center">{title}</h1>
152
138
  </div>
153
139
  )}
154
140
  </header>
@@ -25,7 +25,7 @@ export const SettingsMenu: FC = () => {
25
25
  return (
26
26
  <DropdownMenu>
27
27
  <DropdownMenuTrigger asChild>
28
- <Button variant="ghost" rounded="full" size="icon">
28
+ <Button rounded="full" size="sm" className="w-8" variant="ghost">
29
29
  <Settings className="!size-5" />
30
30
  </Button>
31
31
 
@@ -22,7 +22,7 @@ export const PageWrapper = ({
22
22
  renderRestrictionMenu,
23
23
  restrictField,
24
24
  requestType,
25
- itemsToRender,
25
+ itemsByRow,
26
26
  onlyUsedEntries = true,
27
27
  enableHierarchy = false,
28
28
  fetchMode = "deferred",
@@ -33,7 +33,7 @@ export const PageWrapper = ({
33
33
  const restrictionMenuProps: ComponentProps<typeof TaxonomyRestrictionMenu> = {
34
34
  restrictField: restrictField ?? "informationSubjects",
35
35
  requestType: requestType ?? "InformationSubjectsGetAllClient",
36
- itemsToRender,
36
+ itemsByRow,
37
37
  onlyUsedEntries,
38
38
  enableHierarchy,
39
39
  fetchMode,
@@ -46,7 +46,7 @@ export const PageWrapper = ({
46
46
  <NavBar showInput={showInput} showPkgFilter={showPkgFilter} {...props} />
47
47
 
48
48
  {showRestrictMenu && (
49
- <div className="flex-1 container pt-6">
49
+ <div className="container pt-4">
50
50
  {renderRestrictionMenu ? renderRestrictionMenu(restrictionMenuProps) : (
51
51
  <TaxonomyRestrictionMenu {...restrictionMenuProps} />
52
52
  )}
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
4
4
  import { FragmentsGetByIdClient } from "../generated/client-components";
5
5
  import type { RenditionModel } from "@c-rex/interfaces";
6
6
  import { RenderArticle } from "../render-article";
7
+ import { useTranslations } from "next-intl";
7
8
 
8
9
  type HtmlRenditionClientProps = {
9
10
  htmlFormats?: string[];
@@ -11,8 +12,6 @@ type HtmlRenditionClientProps = {
11
12
  renditions?: RenditionModel[] | null;
12
13
  };
13
14
 
14
- const EMPTY = <div>No rendition available</div>;
15
-
16
15
  const findHtmlViewUrl = (
17
16
  renditions?: RenditionModel[] | null,
18
17
  htmlFormats: string[] = ["application/xhtml+xml", "application/html", "text/html"]
@@ -30,6 +29,7 @@ const HtmlFromRenditions = ({
30
29
  renditions?: RenditionModel[] | null;
31
30
  htmlFormats?: string[];
32
31
  }) => {
32
+ const t = useTranslations();
33
33
  const [htmlContent, setHtmlContent] = useState<string | null>(null);
34
34
  const [hasError, setHasError] = useState(false);
35
35
  const viewUrl = useMemo(() => findHtmlViewUrl(renditions, htmlFormats), [htmlFormats, renditions]);
@@ -62,8 +62,8 @@ const HtmlFromRenditions = ({
62
62
  };
63
63
  }, [viewUrl]);
64
64
 
65
- if (!viewUrl || hasError) return EMPTY;
66
- if (htmlContent == null) return <div className="text-muted-foreground text-sm">Loading content...</div>;
65
+ if (!viewUrl || hasError) return <div>{t("noRenditionAvailable")}</div>;
66
+ if (htmlContent == null) return <div className="text-muted-foreground text-sm">{t("loadingContent")}</div>;
67
67
 
68
68
  return <RenderArticle htmlContent={htmlContent} />;
69
69
  };
@@ -73,11 +73,13 @@ export const HtmlRenditionClient = ({
73
73
  htmlFormats = ["application/xhtml+xml", "application/html", "text/html"],
74
74
  renditions,
75
75
  }: HtmlRenditionClientProps) => {
76
+ const t = useTranslations();
77
+
76
78
  if (renditions !== undefined) {
77
79
  return <HtmlFromRenditions renditions={renditions} htmlFormats={htmlFormats} />;
78
80
  }
79
81
 
80
- if (!fragmentShortId) return EMPTY;
82
+ if (!fragmentShortId) return <div>{t("noRenditionAvailable")}</div>;
81
83
 
82
84
  return (
83
85
  <FragmentsGetByIdClient
@@ -90,7 +92,7 @@ export const HtmlRenditionClient = ({
90
92
  >
91
93
  {({ data, isLoading }) => {
92
94
  if (isLoading && !data) {
93
- return <div className="text-muted-foreground text-sm">Loading content...</div>;
95
+ return <div className="text-muted-foreground text-sm">{t("loadingContent")}</div>;
94
96
  }
95
97
  return <HtmlFromRenditions renditions={data?.renditions} htmlFormats={htmlFormats} />;
96
98
  }}
@@ -3,6 +3,7 @@ import * as cheerio from "cheerio"
3
3
  import { RenditionModel } from "@c-rex/interfaces";
4
4
  import { fragmentsGetByIdServer } from "@c-rex/services/server-requests";
5
5
  import { call } from "@c-rex/utils";
6
+ import { getTranslations } from "next-intl/server";
6
7
 
7
8
  interface HtmlRenditionProps {
8
9
  htmlFormats?: string[]
@@ -23,7 +24,8 @@ export const HtmlRendition: FC<HtmlRenditionProps> = async ({
23
24
  render = defaultRender,
24
25
  renditions,
25
26
  }) => {
26
- const empty = <div>No rendition available</div>;
27
+ const t = await getTranslations();
28
+ const empty = <div>{t("noRenditionAvailable")}</div>;
27
29
 
28
30
  if (renditions == undefined) {
29
31
  if (fragmentShortId) {