@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.
- package/package.json +32 -36
- package/src/article/article-action-bar.tsx +12 -2
- package/src/{check-article-lang.tsx → article/check-article-lang.tsx} +1 -1
- package/src/article/render-article-highlight.tsx +108 -0
- package/src/article/render-article.tsx +28 -0
- package/src/autocomplete.tsx +7 -25
- package/src/blog/blog-author-card.tsx +116 -0
- package/src/carousel/carousel.tsx +5 -2
- package/src/carousel/information-unit-carousel-item.tsx +1 -1
- package/src/content-unavailable.tsx +20 -0
- package/src/directoryNodes/directory-tree-context.tsx +9 -4
- package/src/documents/description-preview.tsx +14 -4
- package/src/documents/result-list-item.tsx +40 -46
- package/src/favorites/__tests__/favorites-hydration.test.tsx +245 -0
- package/src/favorites/bookmark-button.tsx +38 -20
- package/src/favorites/favorite-button.tsx +23 -24
- package/src/favorites/favorites-context.tsx +287 -0
- package/src/icons/file-icon.tsx +9 -26
- package/src/info/information-unit-metadata-grid-client.tsx +21 -21
- package/src/navbar/navbar.tsx +16 -30
- package/src/navbar/settings.tsx +1 -1
- package/src/page-wrapper.tsx +3 -3
- package/src/renditions/html-client.tsx +8 -6
- package/src/renditions/html.tsx +3 -1
- package/src/restriction-menu/restriction-menu-item.tsx +48 -58
- package/src/restriction-menu/restriction-selection-command-menu.tsx +445 -0
- package/src/restriction-menu/restriction-selection-menu.tsx +5 -7
- package/src/restriction-menu/taxonomy-restriction-command-menu.tsx +111 -0
- package/src/restriction-menu/taxonomy-restriction-menu.tsx +19 -12
- package/src/results/filter-navbar.tsx +81 -76
- package/src/results/filter-sidebar/context.tsx +32 -0
- package/src/results/filter-sidebar/index.tsx +40 -35
- package/src/results/generic/search-results-client.tsx +5 -4
- package/src/results/generic/table-result-list.tsx +16 -16
- package/src/results/information-unit-search-results-card-list.tsx +4 -1
- package/src/results/information-unit-search-results-cards.tsx +169 -69
- package/src/results/pagination.tsx +43 -40
- package/src/search-input.tsx +4 -2
- package/src/toc/toc-breadcrumb.tsx +1 -1
- package/src/toc/toc-browse-controls.tsx +2 -2
- package/src/toc/toc-tree-panel.tsx +19 -16
- package/src/article/article-content.tsx +0 -19
- package/src/breadcrumb.tsx +0 -124
- package/src/directoryNodes/tree-of-content.tsx +0 -68
- package/src/render-article.tsx +0 -75
- package/src/restriction-menu/restriction-menu-container.tsx +0 -4
- package/src/restriction-menu/restriction-menu.tsx +0 -4
- package/src/stores/__tests__/favorites-store.test.ts +0 -54
- package/src/stores/favorites-store.ts +0 -163
- /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
|
+
);
|
package/src/icons/file-icon.tsx
CHANGED
|
@@ -34,34 +34,25 @@ const getGenericDisplayCode = (mimeType: string) => {
|
|
|
34
34
|
|
|
35
35
|
const GenericFileTypeIcon = ({ code }: { code: string }) => (
|
|
36
36
|
<svg
|
|
37
|
-
|
|
37
|
+
strokeWidth="0"
|
|
38
|
+
viewBox="0 0 256 256"
|
|
38
39
|
className="size-5 text-primary"
|
|
39
40
|
aria-hidden="true"
|
|
40
|
-
fill="
|
|
41
|
+
fill="currentColor"
|
|
41
42
|
xmlns="http://www.w3.org/2000/svg"
|
|
42
43
|
>
|
|
43
44
|
<path
|
|
44
|
-
d="
|
|
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="
|
|
59
|
-
y="
|
|
48
|
+
x="128"
|
|
49
|
+
y="220"
|
|
60
50
|
textAnchor="middle"
|
|
61
|
-
fontSize="
|
|
51
|
+
fontSize="110"
|
|
62
52
|
fontWeight="700"
|
|
63
53
|
fill="currentColor"
|
|
64
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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}
|
package/src/navbar/navbar.tsx
CHANGED
|
@@ -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
|
|
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-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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>
|
package/src/navbar/settings.tsx
CHANGED
|
@@ -25,7 +25,7 @@ export const SettingsMenu: FC = () => {
|
|
|
25
25
|
return (
|
|
26
26
|
<DropdownMenu>
|
|
27
27
|
<DropdownMenuTrigger asChild>
|
|
28
|
-
<Button
|
|
28
|
+
<Button rounded="full" size="sm" className="w-8" variant="ghost">
|
|
29
29
|
<Settings className="!size-5" />
|
|
30
30
|
</Button>
|
|
31
31
|
|
package/src/page-wrapper.tsx
CHANGED
|
@@ -22,7 +22,7 @@ export const PageWrapper = ({
|
|
|
22
22
|
renderRestrictionMenu,
|
|
23
23
|
restrictField,
|
|
24
24
|
requestType,
|
|
25
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
|
66
|
-
if (htmlContent == null) return <div className="text-muted-foreground text-sm">
|
|
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
|
|
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">
|
|
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
|
}}
|
package/src/renditions/html.tsx
CHANGED
|
@@ -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
|
|
27
|
+
const t = await getTranslations();
|
|
28
|
+
const empty = <div>{t("noRenditionAvailable")}</div>;
|
|
27
29
|
|
|
28
30
|
if (renditions == undefined) {
|
|
29
31
|
if (fragmentShortId) {
|