@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
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.38",
|
|
4
4
|
"files": [
|
|
5
5
|
"src"
|
|
6
6
|
],
|
|
@@ -21,10 +21,6 @@
|
|
|
21
21
|
"types": "./src/generated/suggestions.tsx",
|
|
22
22
|
"import": "./src/generated/suggestions.tsx"
|
|
23
23
|
},
|
|
24
|
-
"./breadcrumb": {
|
|
25
|
-
"types": "./src/breadcrumb.tsx",
|
|
26
|
-
"import": "./src/breadcrumb.tsx"
|
|
27
|
-
},
|
|
28
24
|
"./navbar": {
|
|
29
25
|
"types": "./src/navbar/navbar.tsx",
|
|
30
26
|
"import": "./src/navbar/navbar.tsx"
|
|
@@ -62,12 +58,12 @@
|
|
|
62
58
|
"import": "./src/info/info-card.tsx"
|
|
63
59
|
},
|
|
64
60
|
"./check-article-lang": {
|
|
65
|
-
"types": "./src/check-article-lang.tsx",
|
|
66
|
-
"import": "./src/check-article-lang.tsx"
|
|
61
|
+
"types": "./src/article/check-article-lang.tsx",
|
|
62
|
+
"import": "./src/article/check-article-lang.tsx"
|
|
67
63
|
},
|
|
68
64
|
"./render-article": {
|
|
69
|
-
"types": "./src/render-article.tsx",
|
|
70
|
-
"import": "./src/render-article.tsx"
|
|
65
|
+
"types": "./src/article/render-article.tsx",
|
|
66
|
+
"import": "./src/article/render-article.tsx"
|
|
71
67
|
},
|
|
72
68
|
"./article-action-bar": {
|
|
73
69
|
"types": "./src/article/article-action-bar.tsx",
|
|
@@ -98,17 +94,13 @@
|
|
|
98
94
|
"import": "./src/renditions/html-client.tsx"
|
|
99
95
|
},
|
|
100
96
|
"./image-rendition-container": {
|
|
101
|
-
"types": "./src/renditions/container.
|
|
102
|
-
"import": "./src/renditions/container.
|
|
97
|
+
"types": "./src/renditions/image/container.tsx",
|
|
98
|
+
"import": "./src/renditions/image/container.tsx"
|
|
103
99
|
},
|
|
104
100
|
"./autocomplete": {
|
|
105
101
|
"types": "./src/autocomplete.tsx",
|
|
106
102
|
"import": "./src/autocomplete.tsx"
|
|
107
103
|
},
|
|
108
|
-
"./result-container": {
|
|
109
|
-
"types": "./src/result-container.tsx",
|
|
110
|
-
"import": "./src/result-container.tsx"
|
|
111
|
-
},
|
|
112
104
|
"./dialog-filter": {
|
|
113
105
|
"types": "./src/results/dialog-filter.tsx",
|
|
114
106
|
"import": "./src/results/dialog-filter.tsx"
|
|
@@ -117,6 +109,10 @@
|
|
|
117
109
|
"types": "./src/results/pagination.tsx",
|
|
118
110
|
"import": "./src/results/pagination.tsx"
|
|
119
111
|
},
|
|
112
|
+
"./information-unit-search-results-card-list": {
|
|
113
|
+
"types": "./src/results/information-unit-search-results-card-list.tsx",
|
|
114
|
+
"import": "./src/results/information-unit-search-results-card-list.tsx"
|
|
115
|
+
},
|
|
120
116
|
"./information-unit-search-results-cards": {
|
|
121
117
|
"types": "./src/results/information-unit-search-results-cards.tsx",
|
|
122
118
|
"import": "./src/results/information-unit-search-results-cards.tsx"
|
|
@@ -141,10 +137,10 @@
|
|
|
141
137
|
"types": "./src/stores/highlight-store.ts",
|
|
142
138
|
"import": "./src/stores/highlight-store.ts"
|
|
143
139
|
},
|
|
144
|
-
"./favorites-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
140
|
+
"./favorites-context": {
|
|
141
|
+
"types": "./src/favorites/favorites-context.tsx",
|
|
142
|
+
"import": "./src/favorites/favorites-context.tsx"
|
|
143
|
+
},
|
|
148
144
|
"./search-settings-store": {
|
|
149
145
|
"types": "./src/stores/search-settings-store.ts",
|
|
150
146
|
"import": "./src/stores/search-settings-store.ts"
|
|
@@ -177,14 +173,6 @@
|
|
|
177
173
|
"types": "./src/renditions/image/rendition.tsx",
|
|
178
174
|
"import": "./src/renditions/image/rendition.tsx"
|
|
179
175
|
},
|
|
180
|
-
"./server-image-rendition": {
|
|
181
|
-
"types": "./src/renditions/image/server-image-rendition.tsx",
|
|
182
|
-
"import": "./src/renditions/image/server-image-rendition.tsx"
|
|
183
|
-
},
|
|
184
|
-
"./article-skeleton": {
|
|
185
|
-
"types": "./src/article/article-skeleton.tsx",
|
|
186
|
-
"import": "./src/article/article-skeleton.tsx"
|
|
187
|
-
},
|
|
188
176
|
"./documents-result-list": {
|
|
189
177
|
"types": "./src/documents/result-list.tsx",
|
|
190
178
|
"import": "./src/documents/result-list.tsx"
|
|
@@ -201,14 +189,6 @@
|
|
|
201
189
|
"types": "./src/carousel/carousel.tsx",
|
|
202
190
|
"import": "./src/carousel/carousel.tsx"
|
|
203
191
|
},
|
|
204
|
-
"./restriction-menu": {
|
|
205
|
-
"types": "./src/restriction-menu/restriction-menu.tsx",
|
|
206
|
-
"import": "./src/restriction-menu/restriction-menu.tsx"
|
|
207
|
-
},
|
|
208
|
-
"./restriction-menu-container": {
|
|
209
|
-
"types": "./src/restriction-menu/restriction-menu-container.tsx",
|
|
210
|
-
"import": "./src/restriction-menu/restriction-menu-container.tsx"
|
|
211
|
-
},
|
|
212
192
|
"./restriction-menu-item": {
|
|
213
193
|
"types": "./src/restriction-menu/restriction-menu-item.tsx",
|
|
214
194
|
"import": "./src/restriction-menu/restriction-menu-item.tsx"
|
|
@@ -217,6 +197,10 @@
|
|
|
217
197
|
"types": "./src/search-input.tsx",
|
|
218
198
|
"import": "./src/search-input.tsx"
|
|
219
199
|
},
|
|
200
|
+
"./content-unavailable": {
|
|
201
|
+
"types": "./src/content-unavailable.tsx",
|
|
202
|
+
"import": "./src/content-unavailable.tsx"
|
|
203
|
+
},
|
|
220
204
|
"./footer": {
|
|
221
205
|
"types": "./src/footer/footer.tsx",
|
|
222
206
|
"import": "./src/footer/footer.tsx"
|
|
@@ -240,7 +224,19 @@
|
|
|
240
224
|
"./taxonomy-restriction-menu": {
|
|
241
225
|
"types": "./src/restriction-menu/taxonomy-restriction-menu.tsx",
|
|
242
226
|
"import": "./src/restriction-menu/taxonomy-restriction-menu.tsx"
|
|
243
|
-
}
|
|
227
|
+
},
|
|
228
|
+
"./restriction-selection-command-menu": {
|
|
229
|
+
"types": "./src/restriction-menu/restriction-selection-command-menu.tsx",
|
|
230
|
+
"import": "./src/restriction-menu/restriction-selection-command-menu.tsx"
|
|
231
|
+
},
|
|
232
|
+
"./taxonomy-restriction-command-menu": {
|
|
233
|
+
"types": "./src/restriction-menu/taxonomy-restriction-command-menu.tsx",
|
|
234
|
+
"import": "./src/restriction-menu/taxonomy-restriction-command-menu.tsx"
|
|
235
|
+
},
|
|
236
|
+
"./blog-author-card": {
|
|
237
|
+
"types": "./src/blog/blog-author-card.tsx",
|
|
238
|
+
"import": "./src/blog/blog-author-card.tsx"
|
|
239
|
+
}
|
|
244
240
|
},
|
|
245
241
|
"scripts": {
|
|
246
242
|
"storybook": "storybook dev -p 6006",
|
|
@@ -10,12 +10,15 @@ import { cn } from "@c-rex/utils";
|
|
|
10
10
|
import { useTranslations } from "next-intl";
|
|
11
11
|
import { FavoriteButton } from "../favorites/favorite-button";
|
|
12
12
|
import { ResultTypes } from "@c-rex/types";
|
|
13
|
+
import { ShareButton } from "../share-button";
|
|
13
14
|
type Props = {
|
|
14
15
|
id: string;
|
|
15
16
|
articleType: ResultTypes;
|
|
16
17
|
favoriteLabel: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
children?: React.ReactNode;
|
|
17
20
|
}
|
|
18
|
-
export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel }) => {
|
|
21
|
+
export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel, className, children }) => {
|
|
19
22
|
const t = useTranslations();
|
|
20
23
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
21
24
|
const { next } = useHighlight();
|
|
@@ -31,7 +34,10 @@ export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel })
|
|
|
31
34
|
}, [open]);
|
|
32
35
|
|
|
33
36
|
return (
|
|
34
|
-
<div className=
|
|
37
|
+
<div className={cn(
|
|
38
|
+
"w-9 flex gap-2 transition-all duration-300 z-20 items-end flex-col sticky top-32 self-start",
|
|
39
|
+
className
|
|
40
|
+
)}>
|
|
35
41
|
|
|
36
42
|
<SidebarTrigger side="right" />
|
|
37
43
|
|
|
@@ -41,6 +47,8 @@ export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel })
|
|
|
41
47
|
label={favoriteLabel}
|
|
42
48
|
/>
|
|
43
49
|
|
|
50
|
+
<ShareButton />
|
|
51
|
+
|
|
44
52
|
<Button
|
|
45
53
|
variant="ghost"
|
|
46
54
|
size="icon"
|
|
@@ -98,6 +106,8 @@ export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel })
|
|
|
98
106
|
|
|
99
107
|
</>
|
|
100
108
|
)}
|
|
109
|
+
|
|
110
|
+
{children}
|
|
101
111
|
</div>
|
|
102
112
|
)
|
|
103
113
|
}
|
|
@@ -5,7 +5,7 @@ import { useAppConfig } from "@c-rex/contexts/config-provider";
|
|
|
5
5
|
import { AvailableVersionsInterface } from "@c-rex/interfaces";
|
|
6
6
|
import { toast } from "sonner"
|
|
7
7
|
import { useTranslations } from "next-intl"
|
|
8
|
-
import { useSearchSettingsStore } from "
|
|
8
|
+
import { useSearchSettingsStore } from "../stores/search-settings-store";
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
11
|
availableVersions: AvailableVersionsInterface[]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useQueryState } from "nuqs";
|
|
5
|
+
import { useHighlight } from "@c-rex/contexts/highlight-provider";
|
|
6
|
+
import { useHighlightStore } from "../stores/highlight-store";
|
|
7
|
+
|
|
8
|
+
const EXCLUDED_TAGS = new Set(["MARK", "SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA"]);
|
|
9
|
+
|
|
10
|
+
export const extractHighlightTerms = (query: string | null): string[] => {
|
|
11
|
+
if (!query) return [];
|
|
12
|
+
|
|
13
|
+
return query
|
|
14
|
+
.split(/[+ ]/)
|
|
15
|
+
.map((term) => term.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const applyHighlightToElement = (container: HTMLElement, terms: string[]) => {
|
|
20
|
+
if (terms.length === 0) return;
|
|
21
|
+
|
|
22
|
+
const escapedTerms = terms.map((term) =>
|
|
23
|
+
term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
24
|
+
);
|
|
25
|
+
const regex = new RegExp(`(${escapedTerms.join("|")})`, "gi");
|
|
26
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
27
|
+
const textNodes: Text[] = [];
|
|
28
|
+
|
|
29
|
+
let currentNode = walker.nextNode();
|
|
30
|
+
while (currentNode) {
|
|
31
|
+
const parent = currentNode.parentElement;
|
|
32
|
+
const textContent = currentNode.textContent ?? "";
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
parent &&
|
|
36
|
+
!EXCLUDED_TAGS.has(parent.tagName) &&
|
|
37
|
+
textContent.trim().length > 0
|
|
38
|
+
) {
|
|
39
|
+
const hasMatch = regex.test(textContent);
|
|
40
|
+
regex.lastIndex = 0;
|
|
41
|
+
|
|
42
|
+
if (hasMatch) {
|
|
43
|
+
textNodes.push(currentNode as Text);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
currentNode = walker.nextNode();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
textNodes.forEach((textNode) => {
|
|
51
|
+
const textContent = textNode.textContent ?? "";
|
|
52
|
+
const parts = textContent.split(regex);
|
|
53
|
+
|
|
54
|
+
if (parts.length <= 1) return;
|
|
55
|
+
|
|
56
|
+
const fragment = document.createDocumentFragment();
|
|
57
|
+
|
|
58
|
+
parts.forEach((part) => {
|
|
59
|
+
if (!part) return;
|
|
60
|
+
|
|
61
|
+
const isMatch = regex.test(part);
|
|
62
|
+
regex.lastIndex = 0;
|
|
63
|
+
|
|
64
|
+
if (isMatch) {
|
|
65
|
+
const mark = document.createElement("mark");
|
|
66
|
+
mark.className = "bg-yellow-200";
|
|
67
|
+
mark.textContent = part;
|
|
68
|
+
fragment.appendChild(mark);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fragment.appendChild(document.createTextNode(part));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
textNode.parentNode?.replaceChild(fragment, textNode);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type RenderArticleHighlightProps = {
|
|
80
|
+
containerId: string
|
|
81
|
+
htmlContent: string
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const RenderArticleHighlight = ({
|
|
85
|
+
containerId,
|
|
86
|
+
htmlContent,
|
|
87
|
+
}: RenderArticleHighlightProps) => {
|
|
88
|
+
const [query] = useQueryState("q");
|
|
89
|
+
const enableHighlight = useHighlightStore((state) => state.enable);
|
|
90
|
+
const { registerContainer, refreshMarks } = useHighlight();
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const container = document.getElementById(containerId) as HTMLElement | null;
|
|
94
|
+
if (!container) return;
|
|
95
|
+
|
|
96
|
+
registerContainer(container);
|
|
97
|
+
container.innerHTML = htmlContent;
|
|
98
|
+
|
|
99
|
+
if (enableHighlight) {
|
|
100
|
+
const terms = extractHighlightTerms(query);
|
|
101
|
+
applyHighlightToElement(container, terms);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
refreshMarks();
|
|
105
|
+
}, [containerId, enableHighlight, htmlContent, query, refreshMarks, registerContainer]);
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
import styles from "./render-article.module.css";
|
|
3
|
+
import { RenderArticleHighlight } from "./render-article-highlight";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
htmlContent: string;
|
|
7
|
+
contentLang?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const RenderArticle = ({ htmlContent, contentLang }: Props) => {
|
|
11
|
+
const containerId = useId().replace(/:/g, "");
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<main
|
|
16
|
+
id={containerId}
|
|
17
|
+
data-content-scope="dita"
|
|
18
|
+
lang={contentLang}
|
|
19
|
+
className={`ids-content ids-content--dita-ot ${styles.idsContent}`}
|
|
20
|
+
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
|
21
|
+
/>
|
|
22
|
+
<RenderArticleHighlight
|
|
23
|
+
containerId={containerId}
|
|
24
|
+
htmlContent={htmlContent}
|
|
25
|
+
/>
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
};
|
package/src/autocomplete.tsx
CHANGED
|
@@ -101,27 +101,6 @@ export const AutoComplete = ({
|
|
|
101
101
|
});
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
const clearSearch = () => {
|
|
105
|
-
setQuery("");
|
|
106
|
-
setOpen(false);
|
|
107
|
-
const nextParams = new URLSearchParams(keepParams ? searchParams.toString() : "");
|
|
108
|
-
nextParams.set("page", "1");
|
|
109
|
-
nextParams.delete("search");
|
|
110
|
-
const queryString = nextParams.toString();
|
|
111
|
-
|
|
112
|
-
onSelectParams?.forEach(param => {
|
|
113
|
-
nextParams.set(param.key, param.value);
|
|
114
|
-
});
|
|
115
|
-
const targetUrl = queryString ? `${onSelectPath}?${queryString}` : onSelectPath;
|
|
116
|
-
const currentUrl = `${pathname}${searchParams.toString().length > 0 ? `?${searchParams.toString()}` : ""}`;
|
|
117
|
-
if (targetUrl === currentUrl) return;
|
|
118
|
-
|
|
119
|
-
startSearchNavigation();
|
|
120
|
-
startNavigation(() => {
|
|
121
|
-
router.push(targetUrl);
|
|
122
|
-
});
|
|
123
|
-
};
|
|
124
|
-
|
|
125
104
|
useEffect(() => setQuery(initialValue), [initialValue]);
|
|
126
105
|
useEffect(() => {
|
|
127
106
|
stopSearchNavigation();
|
|
@@ -199,6 +178,7 @@ export const AutoComplete = ({
|
|
|
199
178
|
setQuery(e.target.value);
|
|
200
179
|
setOpen(true);
|
|
201
180
|
}}
|
|
181
|
+
onFocus={() => setOpen(true)}
|
|
202
182
|
/>
|
|
203
183
|
{isNavigating || searchNavigationPending ? (
|
|
204
184
|
<InputGroupAddon align="inline-end">
|
|
@@ -209,8 +189,8 @@ export const AutoComplete = ({
|
|
|
209
189
|
<InputGroupButton
|
|
210
190
|
size="icon-xs"
|
|
211
191
|
variant="ghost"
|
|
212
|
-
aria-label="
|
|
213
|
-
onClick={
|
|
192
|
+
aria-label={t("clearSearch")}
|
|
193
|
+
onClick={() => setQuery("")}
|
|
214
194
|
>
|
|
215
195
|
<X className="size-3" />
|
|
216
196
|
</InputGroupButton>
|
|
@@ -221,7 +201,9 @@ export const AutoComplete = ({
|
|
|
221
201
|
|
|
222
202
|
{open && (
|
|
223
203
|
<ul className="suggestions-list absolute z-10 w-full bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
|
224
|
-
{
|
|
204
|
+
{query.length === 0 ? (
|
|
205
|
+
<li className="px-4 py-2 text-sm text-muted-foreground">{t("typingHint")}</li>
|
|
206
|
+
) : loading ? (
|
|
225
207
|
<li>
|
|
226
208
|
<div className="flex items-center justify-center py-4">
|
|
227
209
|
<div className="animate-spin rounded-full h-6 w-6 border-2 border-gray-300 border-t-gray-950" />
|
|
@@ -246,7 +228,7 @@ export const AutoComplete = ({
|
|
|
246
228
|
))}
|
|
247
229
|
</>
|
|
248
230
|
) : (
|
|
249
|
-
<li className="px-4 py-2">
|
|
231
|
+
<li className="px-4 py-2">{t("noSuggestions")}</li>
|
|
250
232
|
)}
|
|
251
233
|
</>
|
|
252
234
|
)}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { User, Mail, Link as LinkIcon } from "lucide-react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { cn } from "@c-rex/utils";
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@c-rex/ui/card";
|
|
5
|
+
|
|
6
|
+
export type BlogAuthor = {
|
|
7
|
+
name: string;
|
|
8
|
+
photo: string | null;
|
|
9
|
+
title?: string | null;
|
|
10
|
+
role?: string | null;
|
|
11
|
+
emails?: string[];
|
|
12
|
+
urls?: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
authors: BlogAuthor[];
|
|
17
|
+
label: string;
|
|
18
|
+
embedded?: boolean;
|
|
19
|
+
fields?: Array<keyof BlogAuthor>;
|
|
20
|
+
className?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DEFAULT_FIELDS: Array<keyof BlogAuthor> = ["photo", "name", "title", "role"];
|
|
24
|
+
|
|
25
|
+
const AuthorAvatar = ({ author }: { author: BlogAuthor }) => (
|
|
26
|
+
author.photo ? (
|
|
27
|
+
<img
|
|
28
|
+
src={author.photo}
|
|
29
|
+
alt={author.name}
|
|
30
|
+
className="size-10 rounded-full object-cover shrink-0"
|
|
31
|
+
/>
|
|
32
|
+
) : (
|
|
33
|
+
<div className="size-10 rounded-full bg-muted shrink-0 flex items-center justify-center">
|
|
34
|
+
<User className="size-6" />
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const AuthorEntry = ({ author, fields }: { author: BlogAuthor; fields: Array<keyof BlogAuthor> }) => {
|
|
40
|
+
const showPhoto = fields.includes("photo");
|
|
41
|
+
const showName = fields.includes("name");
|
|
42
|
+
const showTitle = fields.includes("title");
|
|
43
|
+
const showRole = fields.includes("role");
|
|
44
|
+
const showEmails = fields.includes("emails");
|
|
45
|
+
const showUrls = fields.includes("urls");
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex items-start gap-3">
|
|
49
|
+
{showPhoto && <AuthorAvatar author={author} />}
|
|
50
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
51
|
+
{showName && (
|
|
52
|
+
<span className="text-sm font-medium truncate">{author.name}</span>
|
|
53
|
+
)}
|
|
54
|
+
{showTitle && author.title && (
|
|
55
|
+
<span className="text-xs text-muted-foreground truncate">{author.title}</span>
|
|
56
|
+
)}
|
|
57
|
+
{showRole && author.role && (
|
|
58
|
+
<span className="text-xs text-muted-foreground truncate">{author.role}</span>
|
|
59
|
+
)}
|
|
60
|
+
{showEmails && author.emails && author.emails.length > 0 && (
|
|
61
|
+
<div className="flex flex-col gap-0.5 mt-1">
|
|
62
|
+
{author.emails.map((email) => (
|
|
63
|
+
<Link
|
|
64
|
+
key={email}
|
|
65
|
+
href={`mailto:${email}`}
|
|
66
|
+
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
|
67
|
+
>
|
|
68
|
+
<Mail className="size-3 shrink-0" />
|
|
69
|
+
<span className="truncate">{email}</span>
|
|
70
|
+
</Link>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
{showUrls && author.urls && author.urls.length > 0 && (
|
|
75
|
+
<div className="flex flex-col gap-0.5 mt-1">
|
|
76
|
+
{author.urls.map((url) => (
|
|
77
|
+
<Link
|
|
78
|
+
key={url}
|
|
79
|
+
href={url}
|
|
80
|
+
target="_blank"
|
|
81
|
+
rel="noopener noreferrer"
|
|
82
|
+
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
|
83
|
+
>
|
|
84
|
+
<LinkIcon className="size-3 shrink-0" />
|
|
85
|
+
<span className="truncate">{url}</span>
|
|
86
|
+
</Link>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const BlogAuthorCard = ({ authors, label, embedded = false, fields = DEFAULT_FIELDS, className }: Props) => {
|
|
96
|
+
if (authors.length === 0) return null;
|
|
97
|
+
|
|
98
|
+
const content = (
|
|
99
|
+
<CardContent className="space-y-3 pb-4">
|
|
100
|
+
{authors.map((author, i) => (
|
|
101
|
+
<AuthorEntry key={i} author={author} fields={fields} />
|
|
102
|
+
))}
|
|
103
|
+
</CardContent>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (embedded) return content;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Card className={cn("gap-0", className)}>
|
|
110
|
+
<CardHeader>
|
|
111
|
+
<CardTitle className="text-lg">{label}</CardTitle>
|
|
112
|
+
</CardHeader>
|
|
113
|
+
{content}
|
|
114
|
+
</Card>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
@@ -298,6 +298,7 @@ export const Carousel: FC<Props> = ({
|
|
|
298
298
|
<div ref={containerRef} className={cn("w-full flex items-center")}>
|
|
299
299
|
{(arrows && data && data.length > 0) && <CarouselPrev />}
|
|
300
300
|
<div className={cn("flex-1 overflow-hidden relative flex items-center")}>
|
|
301
|
+
{/*TODO: update loading position*/}
|
|
301
302
|
{loadByPages && loading && data && data.length > 0 && (
|
|
302
303
|
<div className="absolute right-3 top-3 z-20 rounded-full bg-background/95 p-2 text-muted-foreground shadow-sm">
|
|
303
304
|
<LoaderCircle className="h-3.5 w-3.5 animate-spin" />
|
|
@@ -327,7 +328,10 @@ export const Carousel: FC<Props> = ({
|
|
|
327
328
|
data.map((item: CommonItemsModel, index: number) => (
|
|
328
329
|
<div
|
|
329
330
|
key={item.shortId}
|
|
330
|
-
className={
|
|
331
|
+
className={cn(
|
|
332
|
+
'flex-shrink-0 flex-grow-0 flex justify-center',
|
|
333
|
+
loading && "opacity-40"
|
|
334
|
+
)}
|
|
331
335
|
style={{ width: `${100 / slidesToShow}%` }}
|
|
332
336
|
>
|
|
333
337
|
<RenderComponent
|
|
@@ -454,7 +458,6 @@ const CarouselIndicators: FC<{ className?: string }> = ({ className }) => {
|
|
|
454
458
|
);
|
|
455
459
|
})}
|
|
456
460
|
</ol>
|
|
457
|
-
{pagedMode && loading && <LoaderCircle className="h-3.5 w-3.5 animate-spin text-muted-foreground" />}
|
|
458
461
|
</div>
|
|
459
462
|
);
|
|
460
463
|
};
|
|
@@ -65,7 +65,7 @@ export const InformationUnitCarouselItem: FC<Props> = ({
|
|
|
65
65
|
<Tooltip>
|
|
66
66
|
<TooltipTrigger asChild>
|
|
67
67
|
<div className="h-44 overflow-hidden">
|
|
68
|
-
<span className="line-clamp-5 text-lg font-semibold group-hover:underline">
|
|
68
|
+
<span className="line-clamp-5 text-lg font-semibold group-hover:underline max-w-full overflow-ellipsis [overflow-wrap:anywhere] hyphens-auto">
|
|
69
69
|
{title}
|
|
70
70
|
</span>
|
|
71
71
|
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ComponentProps, FC } from "react";
|
|
2
|
+
import { PageWrapper } from "@c-rex/components/page-wrapper";
|
|
3
|
+
import { getTranslations } from "next-intl/server";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
type Props = Omit<ComponentProps<typeof PageWrapper>, "children">;
|
|
7
|
+
|
|
8
|
+
export const ContentUnavailable: FC<Props> = async ({ ...props }) => {
|
|
9
|
+
const t = await getTranslations();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<PageWrapper {...props}>
|
|
13
|
+
<div className="flex flex-1 justify-center items-center p-4">
|
|
14
|
+
<div className="rounded-md border p-4 text-sm text-muted-foreground">
|
|
15
|
+
{t("contentTemporarilyUnavailableRetry")}
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</PageWrapper>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext, useEffect, useMemo, useState, type FC, type PropsWithChildren } from "react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
4
5
|
import { useRouter } from "next/navigation";
|
|
5
6
|
import type { TreeOfContent } from "@c-rex/interfaces";
|
|
6
7
|
import {
|
|
@@ -232,6 +233,7 @@ type DirectoryTreeSidebarMenuProps = {
|
|
|
232
233
|
export const DirectoryTreeSidebarMenu: FC<DirectoryTreeSidebarMenuProps> = ({
|
|
233
234
|
onNavigateNode,
|
|
234
235
|
}) => {
|
|
236
|
+
const t = useTranslations();
|
|
235
237
|
const { tree, isLoading, expandActivePathOnLoad } = useDirectoryTree();
|
|
236
238
|
const [expandedNodeIds, setExpandedNodeIds] = useState<Set<string>>(new Set());
|
|
237
239
|
|
|
@@ -266,7 +268,8 @@ export const DirectoryTreeSidebarMenu: FC<DirectoryTreeSidebarMenuProps> = ({
|
|
|
266
268
|
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
267
269
|
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
268
270
|
<Skeleton className="w-auto h-10 mb-2" />
|
|
269
|
-
|
|
271
|
+
{/*TODO: need this?*/}
|
|
272
|
+
<div className="px-2 pt-1 text-xs text-muted-foreground">{t("loadingTableOfContents")}</div>
|
|
270
273
|
</div>
|
|
271
274
|
);
|
|
272
275
|
}
|
|
@@ -302,7 +305,7 @@ export const DirectoryTreeSidebarMenu: FC<DirectoryTreeSidebarMenuProps> = ({
|
|
|
302
305
|
{node.children?.length ? (
|
|
303
306
|
<button
|
|
304
307
|
type="button"
|
|
305
|
-
aria-label={expandedNodeIds.has(node.id) ? "
|
|
308
|
+
aria-label={expandedNodeIds.has(node.id) ? t("collapseSection") : t("expandSection")}
|
|
306
309
|
className="h-8 w-8 inline-flex items-center justify-center text-muted-foreground hover:text-foreground"
|
|
307
310
|
onClick={() => toggleNode(node.id)}
|
|
308
311
|
>
|
|
@@ -335,11 +338,13 @@ type DirectoryTreeBreadcrumbProps = {
|
|
|
335
338
|
|
|
336
339
|
export const DirectoryTreeBreadcrumb: FC<DirectoryTreeBreadcrumbProps> = ({
|
|
337
340
|
lang,
|
|
338
|
-
homeLabel
|
|
341
|
+
homeLabel,
|
|
339
342
|
onNavigateHome,
|
|
340
343
|
onNavigateNodeId,
|
|
341
344
|
}) => {
|
|
345
|
+
const t = useTranslations();
|
|
342
346
|
const { tree, isLoading } = useDirectoryTree();
|
|
347
|
+
const resolvedHomeLabel = homeLabel ?? t("home");
|
|
343
348
|
const router = useRouter();
|
|
344
349
|
const items = generateBreadcrumbItems(tree);
|
|
345
350
|
|
|
@@ -362,7 +367,7 @@ export const DirectoryTreeBreadcrumb: FC<DirectoryTreeBreadcrumbProps> = ({
|
|
|
362
367
|
<BreadcrumbList>
|
|
363
368
|
<BreadcrumbItem>
|
|
364
369
|
<button type="button" onClick={navigateHome}>
|
|
365
|
-
{
|
|
370
|
+
{resolvedHomeLabel}
|
|
366
371
|
</button>
|
|
367
372
|
</BreadcrumbItem>
|
|
368
373
|
<BreadcrumbSeparator />
|