@c-rex/components 0.1.38 → 0.1.39
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/README.md +73 -73
- package/package.json +250 -218
- package/src/article/article-action-bar.tsx +110 -110
- package/src/article/article-content.tsx +18 -46
- package/src/autocomplete.tsx +201 -201
- package/src/breadcrumb.tsx +124 -124
- package/src/carousel/carousel.tsx +353 -353
- package/src/check-article-lang.tsx +47 -47
- package/src/directoryNodes/directory-tree-context.tsx +388 -0
- package/src/directoryNodes/tree-of-content.tsx +68 -67
- package/src/documents/result-list.tsx +124 -127
- package/src/favorites/bookmark-button.tsx +97 -94
- package/src/favorites/favorite-button.tsx +137 -120
- package/src/footer/footer-shell.tsx +52 -0
- package/src/footer/footer.tsx +7 -0
- package/src/footer/legal-links-block.tsx +25 -0
- package/src/footer/organization-contact-block.tsx +94 -0
- package/src/footer/social-links-block.tsx +38 -0
- package/src/footer/types.ts +10 -0
- package/src/footer/vcard-footer.tsx +72 -0
- package/src/generated/client-components.tsx +1366 -1350
- package/src/generated/create-client-request.tsx +116 -113
- package/src/generated/create-server-request.tsx +70 -61
- package/src/generated/create-suggestions-request.tsx +55 -55
- package/src/generated/server-components.tsx +1056 -1056
- package/src/generated/suggestions.tsx +302 -299
- package/src/icons/file-icon.tsx +8 -8
- package/src/icons/flag-icon.tsx +15 -15
- package/src/icons/loading.tsx +11 -11
- package/src/icons/social-icon.tsx +24 -0
- package/src/info/info-card.tsx +43 -0
- package/src/info/{info-table.tsx → information-unit-metadata-grid.tsx} +157 -168
- package/src/info/shared.tsx +49 -25
- package/src/navbar/language-switcher/content-language-switch.tsx +92 -92
- package/src/navbar/language-switcher/shared.tsx +33 -33
- package/src/navbar/language-switcher/ui-language-switch.tsx +37 -37
- package/src/navbar/navbar.tsx +157 -152
- package/src/navbar/settings.tsx +62 -62
- package/src/navbar/sign-in-out-btns.tsx +35 -35
- package/src/navbar/user-menu.tsx +60 -60
- package/src/page-wrapper.tsx +54 -31
- package/src/render-article.module.css +155 -0
- package/src/render-article.tsx +75 -68
- package/src/renditions/file-download.tsx +83 -83
- package/src/renditions/html.tsx +64 -64
- package/src/renditions/image/container.tsx +54 -54
- package/src/renditions/image/rendition.tsx +55 -55
- package/src/restriction-menu/restriction-menu-container.tsx +117 -53
- package/src/restriction-menu/restriction-menu-item.tsx +155 -147
- package/src/restriction-menu/restriction-menu.tsx +341 -156
- package/src/results/dialog-filter.tsx +166 -166
- package/src/results/empty.tsx +15 -15
- package/src/results/filter-navbar.tsx +294 -261
- package/src/results/filter-sidebar/__tests__/utils.test.ts +129 -0
- package/src/results/filter-sidebar/index.tsx +270 -126
- package/src/results/filter-sidebar/utils.ts +196 -164
- package/src/results/generic/table-result-list.tsx +97 -99
- package/src/results/{table-with-images.tsx → information-unit-search-results-card-list.tsx} +125 -127
- package/src/results/{cards.tsx → information-unit-search-results-cards.tsx} +99 -99
- package/src/results/{table.tsx → information-unit-search-results-table.tsx} +104 -104
- package/src/results/pagination.tsx +81 -81
- package/src/results/summary.ts +30 -0
- package/src/results/utils.ts +54 -54
- package/src/search-input.tsx +70 -70
- package/src/share-button.tsx +49 -49
- package/src/stores/favorites-store.ts +88 -88
- package/src/stores/highlight-store.ts +15 -15
- package/src/stores/language-store.ts +14 -14
- package/src/stores/restriction-store.ts +11 -11
- package/src/stores/search-settings-store.ts +68 -64
- package/src/info/set-available-versions.tsx +0 -19
package/src/autocomplete.tsx
CHANGED
|
@@ -1,201 +1,201 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
-
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@c-rex/ui/input-group";
|
|
5
|
-
import { useTranslations } from "next-intl";
|
|
6
|
-
import { SuggestionQueryParams } from "@c-rex/interfaces";
|
|
7
|
-
import { X } from "lucide-react";
|
|
8
|
-
import { useRouter, useSearchParams } from "next/navigation";
|
|
9
|
-
import { suggestionRequest } from "./generated/create-suggestions-request";
|
|
10
|
-
import { useQueryState } from "nuqs";
|
|
11
|
-
import { useSearchSettingsStore } from "./stores/search-settings-store";
|
|
12
|
-
|
|
13
|
-
export type AutoCompleteProps = {
|
|
14
|
-
initialValue?: string;
|
|
15
|
-
embedded?: boolean;
|
|
16
|
-
endpoint: string;
|
|
17
|
-
queryParams?: SuggestionQueryParams
|
|
18
|
-
onSelectParams?: { key: string, value: string }[];
|
|
19
|
-
onSelectPath: string
|
|
20
|
-
inputClass?: string
|
|
21
|
-
keepParams?: boolean;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export const AutoComplete = ({
|
|
25
|
-
initialValue = "",
|
|
26
|
-
embedded = true,
|
|
27
|
-
keepParams = false,
|
|
28
|
-
endpoint,
|
|
29
|
-
queryParams,
|
|
30
|
-
onSelectParams,
|
|
31
|
-
onSelectPath = "/",
|
|
32
|
-
inputClass = "",
|
|
33
|
-
}: AutoCompleteProps) => {
|
|
34
|
-
const t = useTranslations();
|
|
35
|
-
const [pkg] = useQueryState("package");
|
|
36
|
-
|
|
37
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
38
|
-
const searchParams = useSearchParams();
|
|
39
|
-
const router = useRouter();
|
|
40
|
-
|
|
41
|
-
const [open, setOpen] = useState(false);
|
|
42
|
-
const [query, setQuery] = useState(initialValue);
|
|
43
|
-
const [loading, setLoading] = useState(false);
|
|
44
|
-
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
45
|
-
|
|
46
|
-
const fetchSuggestions = useCallback(async (prefix: string): Promise<string[]> => {
|
|
47
|
-
const params = { ...queryParams };
|
|
48
|
-
const contentLang = useSearchSettingsStore.getState().language;
|
|
49
|
-
const restrictions = searchParams.get("restrict")
|
|
50
|
-
|
|
51
|
-
if (restrictions) {
|
|
52
|
-
if (restrictions.includes("informationSubjects")) {
|
|
53
|
-
|
|
54
|
-
const informationSubject = restrictions.split("informationSubjects=")[1]?.split(',') || [];
|
|
55
|
-
if (informationSubject.length > 0) {
|
|
56
|
-
params.scopes = informationSubject.map(subject => `informationSubjects=${subject}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (contentLang) params.lang = contentLang;
|
|
62
|
-
if (pkg != null) params.scopes = pkg as unknown as string[];
|
|
63
|
-
|
|
64
|
-
const results = await suggestionRequest({ endpoint, prefix, queryParams: params });
|
|
65
|
-
|
|
66
|
-
return results.data;
|
|
67
|
-
}, [endpoint, pkg, queryParams, searchParams]);
|
|
68
|
-
|
|
69
|
-
const handleSelect = (value: string) => {
|
|
70
|
-
setQuery(value);
|
|
71
|
-
setOpen(false);
|
|
72
|
-
|
|
73
|
-
const nextParams = new URLSearchParams(keepParams ? searchParams.toString() : "");
|
|
74
|
-
nextParams.set("page", "1");
|
|
75
|
-
|
|
76
|
-
if (value.length > 0) {
|
|
77
|
-
nextParams.set("search", value);
|
|
78
|
-
} else {
|
|
79
|
-
nextParams.delete("search");
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
onSelectParams?.forEach(param => {
|
|
83
|
-
nextParams.set(param.key, param.value);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
router.push(`${onSelectPath}?${nextParams.toString()}`);
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const clearSearch = () => {
|
|
90
|
-
setQuery("");
|
|
91
|
-
setOpen(false);
|
|
92
|
-
const nextParams = new URLSearchParams(keepParams ? searchParams.toString() : "");
|
|
93
|
-
nextParams.set("page", "1");
|
|
94
|
-
nextParams.delete("search");
|
|
95
|
-
const queryString = nextParams.toString();
|
|
96
|
-
|
|
97
|
-
onSelectParams?.forEach(param => {
|
|
98
|
-
nextParams.set(param.key, param.value);
|
|
99
|
-
});
|
|
100
|
-
router.push(queryString ? `${onSelectPath}?${queryString}` : onSelectPath);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
useEffect(() => setQuery(initialValue), [initialValue]);
|
|
104
|
-
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
107
|
-
if (!containerRef.current?.contains(e.target as Node)) {
|
|
108
|
-
setOpen(false);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
112
|
-
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
113
|
-
}, []);
|
|
114
|
-
|
|
115
|
-
useEffect(() => {
|
|
116
|
-
if (query.length < 2) {
|
|
117
|
-
setSuggestions([]);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const debounceFetch = setTimeout(() => {
|
|
122
|
-
setLoading(true);
|
|
123
|
-
fetchSuggestions(query)
|
|
124
|
-
.then(setSuggestions)
|
|
125
|
-
.catch(() => setSuggestions([]))
|
|
126
|
-
.finally(() => setLoading(false));
|
|
127
|
-
}, 300);
|
|
128
|
-
|
|
129
|
-
return () => clearTimeout(debounceFetch);
|
|
130
|
-
}, [query, fetchSuggestions]);
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<div className="relative flex-1" ref={containerRef}>
|
|
134
|
-
<InputGroup variant={embedded ? "embedded" : "default"}>
|
|
135
|
-
<InputGroupInput
|
|
136
|
-
variant={embedded ? "embedded" : undefined}
|
|
137
|
-
className={inputClass}
|
|
138
|
-
type="text"
|
|
139
|
-
placeholder={t("search")}
|
|
140
|
-
value={query}
|
|
141
|
-
onChange={(e) => {
|
|
142
|
-
setQuery(e.target.value);
|
|
143
|
-
setOpen(true);
|
|
144
|
-
}}
|
|
145
|
-
onKeyDown={(e) => {
|
|
146
|
-
if (e.key === "Enter") {
|
|
147
|
-
e.preventDefault();
|
|
148
|
-
handleSelect(query);
|
|
149
|
-
}
|
|
150
|
-
}}
|
|
151
|
-
/>
|
|
152
|
-
{query.length > 0 && (
|
|
153
|
-
<InputGroupAddon align="inline-end">
|
|
154
|
-
<InputGroupButton
|
|
155
|
-
size="icon-xs"
|
|
156
|
-
variant="ghost"
|
|
157
|
-
aria-label="Clear search"
|
|
158
|
-
onClick={clearSearch}
|
|
159
|
-
>
|
|
160
|
-
<X className="size-3" />
|
|
161
|
-
</InputGroupButton>
|
|
162
|
-
</InputGroupAddon>
|
|
163
|
-
)}
|
|
164
|
-
</InputGroup>
|
|
165
|
-
|
|
166
|
-
{open && (
|
|
167
|
-
<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">
|
|
168
|
-
{loading ? (
|
|
169
|
-
<li>
|
|
170
|
-
<div className="flex items-center justify-center py-4">
|
|
171
|
-
<div className="animate-spin rounded-full h-6 w-6 border-2 border-gray-300 border-t-gray-950" />
|
|
172
|
-
</div>
|
|
173
|
-
</li>
|
|
174
|
-
) : (
|
|
175
|
-
<>
|
|
176
|
-
{suggestions.length > 0 ? (
|
|
177
|
-
<>
|
|
178
|
-
{suggestions.map((option) => (
|
|
179
|
-
<li
|
|
180
|
-
key={option}
|
|
181
|
-
className="px-4 py-2 hover:bg-accent cursor-pointer text-sm"
|
|
182
|
-
onClick={() => {
|
|
183
|
-
//handleSelect(`"${option}"`)
|
|
184
|
-
// TODO: check if the quotes are necessary
|
|
185
|
-
handleSelect(option)
|
|
186
|
-
}}
|
|
187
|
-
>
|
|
188
|
-
{option}
|
|
189
|
-
</li>
|
|
190
|
-
))}
|
|
191
|
-
</>
|
|
192
|
-
) : (
|
|
193
|
-
<li className="px-4 py-2">No suggestions.</li>
|
|
194
|
-
)}
|
|
195
|
-
</>
|
|
196
|
-
)}
|
|
197
|
-
</ul>
|
|
198
|
-
)}
|
|
199
|
-
</div >
|
|
200
|
-
);
|
|
201
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@c-rex/ui/input-group";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { SuggestionQueryParams } from "@c-rex/interfaces";
|
|
7
|
+
import { X } from "lucide-react";
|
|
8
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
9
|
+
import { suggestionRequest } from "./generated/create-suggestions-request";
|
|
10
|
+
import { useQueryState } from "nuqs";
|
|
11
|
+
import { useSearchSettingsStore } from "./stores/search-settings-store";
|
|
12
|
+
|
|
13
|
+
export type AutoCompleteProps = {
|
|
14
|
+
initialValue?: string;
|
|
15
|
+
embedded?: boolean;
|
|
16
|
+
endpoint: string;
|
|
17
|
+
queryParams?: SuggestionQueryParams
|
|
18
|
+
onSelectParams?: { key: string, value: string }[];
|
|
19
|
+
onSelectPath: string
|
|
20
|
+
inputClass?: string
|
|
21
|
+
keepParams?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const AutoComplete = ({
|
|
25
|
+
initialValue = "",
|
|
26
|
+
embedded = true,
|
|
27
|
+
keepParams = false,
|
|
28
|
+
endpoint,
|
|
29
|
+
queryParams,
|
|
30
|
+
onSelectParams,
|
|
31
|
+
onSelectPath = "/",
|
|
32
|
+
inputClass = "",
|
|
33
|
+
}: AutoCompleteProps) => {
|
|
34
|
+
const t = useTranslations();
|
|
35
|
+
const [pkg] = useQueryState("package");
|
|
36
|
+
|
|
37
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
38
|
+
const searchParams = useSearchParams();
|
|
39
|
+
const router = useRouter();
|
|
40
|
+
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const [query, setQuery] = useState(initialValue);
|
|
43
|
+
const [loading, setLoading] = useState(false);
|
|
44
|
+
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
45
|
+
|
|
46
|
+
const fetchSuggestions = useCallback(async (prefix: string): Promise<string[]> => {
|
|
47
|
+
const params = { ...queryParams };
|
|
48
|
+
const contentLang = useSearchSettingsStore.getState().language;
|
|
49
|
+
const restrictions = searchParams.get("restrict")
|
|
50
|
+
|
|
51
|
+
if (restrictions) {
|
|
52
|
+
if (restrictions.includes("informationSubjects")) {
|
|
53
|
+
|
|
54
|
+
const informationSubject = restrictions.split("informationSubjects=")[1]?.split(',') || [];
|
|
55
|
+
if (informationSubject.length > 0) {
|
|
56
|
+
params.scopes = informationSubject.map(subject => `informationSubjects=${subject}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (contentLang) params.lang = contentLang;
|
|
62
|
+
if (pkg != null) params.scopes = pkg as unknown as string[];
|
|
63
|
+
|
|
64
|
+
const results = await suggestionRequest({ endpoint, prefix, queryParams: params });
|
|
65
|
+
|
|
66
|
+
return results.data;
|
|
67
|
+
}, [endpoint, pkg, queryParams, searchParams]);
|
|
68
|
+
|
|
69
|
+
const handleSelect = (value: string) => {
|
|
70
|
+
setQuery(value);
|
|
71
|
+
setOpen(false);
|
|
72
|
+
|
|
73
|
+
const nextParams = new URLSearchParams(keepParams ? searchParams.toString() : "");
|
|
74
|
+
nextParams.set("page", "1");
|
|
75
|
+
|
|
76
|
+
if (value.length > 0) {
|
|
77
|
+
nextParams.set("search", value);
|
|
78
|
+
} else {
|
|
79
|
+
nextParams.delete("search");
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
onSelectParams?.forEach(param => {
|
|
83
|
+
nextParams.set(param.key, param.value);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
router.push(`${onSelectPath}?${nextParams.toString()}`);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const clearSearch = () => {
|
|
90
|
+
setQuery("");
|
|
91
|
+
setOpen(false);
|
|
92
|
+
const nextParams = new URLSearchParams(keepParams ? searchParams.toString() : "");
|
|
93
|
+
nextParams.set("page", "1");
|
|
94
|
+
nextParams.delete("search");
|
|
95
|
+
const queryString = nextParams.toString();
|
|
96
|
+
|
|
97
|
+
onSelectParams?.forEach(param => {
|
|
98
|
+
nextParams.set(param.key, param.value);
|
|
99
|
+
});
|
|
100
|
+
router.push(queryString ? `${onSelectPath}?${queryString}` : onSelectPath);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
useEffect(() => setQuery(initialValue), [initialValue]);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
107
|
+
if (!containerRef.current?.contains(e.target as Node)) {
|
|
108
|
+
setOpen(false);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
112
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (query.length < 2) {
|
|
117
|
+
setSuggestions([]);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const debounceFetch = setTimeout(() => {
|
|
122
|
+
setLoading(true);
|
|
123
|
+
fetchSuggestions(query)
|
|
124
|
+
.then(setSuggestions)
|
|
125
|
+
.catch(() => setSuggestions([]))
|
|
126
|
+
.finally(() => setLoading(false));
|
|
127
|
+
}, 300);
|
|
128
|
+
|
|
129
|
+
return () => clearTimeout(debounceFetch);
|
|
130
|
+
}, [query, fetchSuggestions]);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="relative flex-1" ref={containerRef}>
|
|
134
|
+
<InputGroup variant={embedded ? "embedded" : "default"}>
|
|
135
|
+
<InputGroupInput
|
|
136
|
+
variant={embedded ? "embedded" : undefined}
|
|
137
|
+
className={inputClass}
|
|
138
|
+
type="text"
|
|
139
|
+
placeholder={t("search")}
|
|
140
|
+
value={query}
|
|
141
|
+
onChange={(e) => {
|
|
142
|
+
setQuery(e.target.value);
|
|
143
|
+
setOpen(true);
|
|
144
|
+
}}
|
|
145
|
+
onKeyDown={(e) => {
|
|
146
|
+
if (e.key === "Enter") {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
handleSelect(query);
|
|
149
|
+
}
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
{query.length > 0 && (
|
|
153
|
+
<InputGroupAddon align="inline-end">
|
|
154
|
+
<InputGroupButton
|
|
155
|
+
size="icon-xs"
|
|
156
|
+
variant="ghost"
|
|
157
|
+
aria-label="Clear search"
|
|
158
|
+
onClick={clearSearch}
|
|
159
|
+
>
|
|
160
|
+
<X className="size-3" />
|
|
161
|
+
</InputGroupButton>
|
|
162
|
+
</InputGroupAddon>
|
|
163
|
+
)}
|
|
164
|
+
</InputGroup>
|
|
165
|
+
|
|
166
|
+
{open && (
|
|
167
|
+
<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">
|
|
168
|
+
{loading ? (
|
|
169
|
+
<li>
|
|
170
|
+
<div className="flex items-center justify-center py-4">
|
|
171
|
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-gray-300 border-t-gray-950" />
|
|
172
|
+
</div>
|
|
173
|
+
</li>
|
|
174
|
+
) : (
|
|
175
|
+
<>
|
|
176
|
+
{suggestions.length > 0 ? (
|
|
177
|
+
<>
|
|
178
|
+
{suggestions.map((option) => (
|
|
179
|
+
<li
|
|
180
|
+
key={option}
|
|
181
|
+
className="px-4 py-2 hover:bg-accent cursor-pointer text-sm"
|
|
182
|
+
onClick={() => {
|
|
183
|
+
//handleSelect(`"${option}"`)
|
|
184
|
+
// TODO: check if the quotes are necessary
|
|
185
|
+
handleSelect(option)
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
{option}
|
|
189
|
+
</li>
|
|
190
|
+
))}
|
|
191
|
+
</>
|
|
192
|
+
) : (
|
|
193
|
+
<li className="px-4 py-2">No suggestions.</li>
|
|
194
|
+
)}
|
|
195
|
+
</>
|
|
196
|
+
)}
|
|
197
|
+
</ul>
|
|
198
|
+
)}
|
|
199
|
+
</div >
|
|
200
|
+
);
|
|
201
|
+
}
|
package/src/breadcrumb.tsx
CHANGED
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { FC, Fragment, ReactNode } from "react";
|
|
4
|
-
import {
|
|
5
|
-
Breadcrumb as BreadcrumbComponent,
|
|
6
|
-
BreadcrumbEllipsis,
|
|
7
|
-
BreadcrumbItem,
|
|
8
|
-
BreadcrumbLink,
|
|
9
|
-
BreadcrumbList,
|
|
10
|
-
BreadcrumbPage,
|
|
11
|
-
BreadcrumbSeparator
|
|
12
|
-
} from "@c-rex/ui/breadcrumb";
|
|
13
|
-
import { Button } from "@c-rex/ui/button";
|
|
14
|
-
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@c-rex/ui/drawer";
|
|
15
|
-
import { TreeOfContent } from "@c-rex/interfaces";
|
|
16
|
-
import { useTranslations } from 'next-intl';
|
|
17
|
-
import Link from "next/link"
|
|
18
|
-
import { useBreakpoint } from "@c-rex/ui/hooks"
|
|
19
|
-
import { DEVICE_OPTIONS } from "@c-rex/constants";
|
|
20
|
-
import { generateBreadcrumbItems } from "@c-rex/utils";
|
|
21
|
-
|
|
22
|
-
interface BreadcrumbProps {
|
|
23
|
-
tree: TreeOfContent[];
|
|
24
|
-
lang: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const Breadcrumb: FC<BreadcrumbProps> = ({ tree, lang }) => {
|
|
28
|
-
const t = useTranslations("breadcrumbs");
|
|
29
|
-
const items = generateBreadcrumbItems(tree);
|
|
30
|
-
const device = useBreakpoint();
|
|
31
|
-
const isMobile = device !== null && (device === DEVICE_OPTIONS.MOBILE || device === DEVICE_OPTIONS.TABLET);
|
|
32
|
-
|
|
33
|
-
if (!items) return null
|
|
34
|
-
|
|
35
|
-
const lastItem = items[items.length - 1] as TreeOfContent;
|
|
36
|
-
|
|
37
|
-
const renderDrawer = () => {
|
|
38
|
-
if (items.length < 2) return null
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<>
|
|
42
|
-
<BreadcrumbItem>
|
|
43
|
-
<Drawer>
|
|
44
|
-
<DrawerTrigger asChild>
|
|
45
|
-
<BreadcrumbEllipsis className="h-4 w-4" />
|
|
46
|
-
</DrawerTrigger>
|
|
47
|
-
<DrawerContent>
|
|
48
|
-
<DrawerHeader className="text-left">
|
|
49
|
-
<DrawerTitle>Navigate to</DrawerTitle>
|
|
50
|
-
<DrawerDescription>
|
|
51
|
-
Select a page to navigate to.
|
|
52
|
-
</DrawerDescription>
|
|
53
|
-
</DrawerHeader>
|
|
54
|
-
|
|
55
|
-
<div className="grid gap-1 px-4">
|
|
56
|
-
{items.slice(0, -1).map((item) => (
|
|
57
|
-
<Link
|
|
58
|
-
key={item.linkId}
|
|
59
|
-
href={`/topics/${item.linkId}/content`}
|
|
60
|
-
className="py-1 text-sm"
|
|
61
|
-
>
|
|
62
|
-
{item.label}
|
|
63
|
-
</Link>
|
|
64
|
-
))}
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<DrawerFooter className="pt-4">
|
|
68
|
-
<DrawerClose asChild>
|
|
69
|
-
<Button variant="outline">Close</Button>
|
|
70
|
-
</DrawerClose>
|
|
71
|
-
</DrawerFooter>
|
|
72
|
-
</DrawerContent>
|
|
73
|
-
</Drawer>
|
|
74
|
-
</BreadcrumbItem>
|
|
75
|
-
<BreadcrumbSeparator />
|
|
76
|
-
</>
|
|
77
|
-
|
|
78
|
-
);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const renderLink = (showLink: boolean, item: TreeOfContent): ReactNode => {
|
|
82
|
-
if (showLink) {
|
|
83
|
-
return <BreadcrumbLink href={`/topics/${item.linkId}/content`}>{item.label}</BreadcrumbLink>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return <BreadcrumbPage>{item.label}</BreadcrumbPage>;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<BreadcrumbComponent lang={lang} className="hidden sm:block">
|
|
91
|
-
<BreadcrumbList>
|
|
92
|
-
<BreadcrumbItem>
|
|
93
|
-
<BreadcrumbLink asChild>
|
|
94
|
-
<BreadcrumbLink href="/">{t("home")}</BreadcrumbLink>
|
|
95
|
-
</BreadcrumbLink>
|
|
96
|
-
</BreadcrumbItem>
|
|
97
|
-
<BreadcrumbSeparator />
|
|
98
|
-
|
|
99
|
-
{isMobile ? renderDrawer() : (
|
|
100
|
-
<>
|
|
101
|
-
{items.slice(0, -1).map((item, index) => {
|
|
102
|
-
const isLast = index === items.length - 1;
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<Fragment key={`${item.label}-fragment`}>
|
|
106
|
-
<BreadcrumbItem key={`${item.label}-item`}>
|
|
107
|
-
{renderLink(!isLast, item)}
|
|
108
|
-
</BreadcrumbItem>
|
|
109
|
-
{!isLast && (
|
|
110
|
-
<BreadcrumbSeparator key={`${item.label}-separator`} />
|
|
111
|
-
)}
|
|
112
|
-
</Fragment>
|
|
113
|
-
);
|
|
114
|
-
})}
|
|
115
|
-
</>
|
|
116
|
-
)}
|
|
117
|
-
|
|
118
|
-
<BreadcrumbPage>
|
|
119
|
-
{renderLink(false, lastItem)}
|
|
120
|
-
</BreadcrumbPage>
|
|
121
|
-
</BreadcrumbList>
|
|
122
|
-
</BreadcrumbComponent>
|
|
123
|
-
);
|
|
124
|
-
};
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { FC, Fragment, ReactNode } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Breadcrumb as BreadcrumbComponent,
|
|
6
|
+
BreadcrumbEllipsis,
|
|
7
|
+
BreadcrumbItem,
|
|
8
|
+
BreadcrumbLink,
|
|
9
|
+
BreadcrumbList,
|
|
10
|
+
BreadcrumbPage,
|
|
11
|
+
BreadcrumbSeparator
|
|
12
|
+
} from "@c-rex/ui/breadcrumb";
|
|
13
|
+
import { Button } from "@c-rex/ui/button";
|
|
14
|
+
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@c-rex/ui/drawer";
|
|
15
|
+
import { TreeOfContent } from "@c-rex/interfaces";
|
|
16
|
+
import { useTranslations } from 'next-intl';
|
|
17
|
+
import Link from "next/link"
|
|
18
|
+
import { useBreakpoint } from "@c-rex/ui/hooks"
|
|
19
|
+
import { DEVICE_OPTIONS } from "@c-rex/constants";
|
|
20
|
+
import { generateBreadcrumbItems } from "@c-rex/utils";
|
|
21
|
+
|
|
22
|
+
interface BreadcrumbProps {
|
|
23
|
+
tree: TreeOfContent[];
|
|
24
|
+
lang: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Breadcrumb: FC<BreadcrumbProps> = ({ tree, lang }) => {
|
|
28
|
+
const t = useTranslations("breadcrumbs");
|
|
29
|
+
const items = generateBreadcrumbItems(tree);
|
|
30
|
+
const device = useBreakpoint();
|
|
31
|
+
const isMobile = device !== null && (device === DEVICE_OPTIONS.MOBILE || device === DEVICE_OPTIONS.TABLET);
|
|
32
|
+
|
|
33
|
+
if (!items) return null
|
|
34
|
+
|
|
35
|
+
const lastItem = items[items.length - 1] as TreeOfContent;
|
|
36
|
+
|
|
37
|
+
const renderDrawer = () => {
|
|
38
|
+
if (items.length < 2) return null
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<BreadcrumbItem>
|
|
43
|
+
<Drawer>
|
|
44
|
+
<DrawerTrigger asChild>
|
|
45
|
+
<BreadcrumbEllipsis className="h-4 w-4" />
|
|
46
|
+
</DrawerTrigger>
|
|
47
|
+
<DrawerContent>
|
|
48
|
+
<DrawerHeader className="text-left">
|
|
49
|
+
<DrawerTitle>Navigate to</DrawerTitle>
|
|
50
|
+
<DrawerDescription>
|
|
51
|
+
Select a page to navigate to.
|
|
52
|
+
</DrawerDescription>
|
|
53
|
+
</DrawerHeader>
|
|
54
|
+
|
|
55
|
+
<div className="grid gap-1 px-4">
|
|
56
|
+
{items.slice(0, -1).map((item) => (
|
|
57
|
+
<Link
|
|
58
|
+
key={item.linkId}
|
|
59
|
+
href={`/topics/${item.linkId}/content`}
|
|
60
|
+
className="py-1 text-sm"
|
|
61
|
+
>
|
|
62
|
+
{item.label}
|
|
63
|
+
</Link>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<DrawerFooter className="pt-4">
|
|
68
|
+
<DrawerClose asChild>
|
|
69
|
+
<Button variant="outline">Close</Button>
|
|
70
|
+
</DrawerClose>
|
|
71
|
+
</DrawerFooter>
|
|
72
|
+
</DrawerContent>
|
|
73
|
+
</Drawer>
|
|
74
|
+
</BreadcrumbItem>
|
|
75
|
+
<BreadcrumbSeparator />
|
|
76
|
+
</>
|
|
77
|
+
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const renderLink = (showLink: boolean, item: TreeOfContent): ReactNode => {
|
|
82
|
+
if (showLink) {
|
|
83
|
+
return <BreadcrumbLink href={`/topics/${item.linkId}/content`}>{item.label}</BreadcrumbLink>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return <BreadcrumbPage>{item.label}</BreadcrumbPage>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<BreadcrumbComponent lang={lang} className="hidden sm:block">
|
|
91
|
+
<BreadcrumbList>
|
|
92
|
+
<BreadcrumbItem>
|
|
93
|
+
<BreadcrumbLink asChild>
|
|
94
|
+
<BreadcrumbLink href="/">{t("home")}</BreadcrumbLink>
|
|
95
|
+
</BreadcrumbLink>
|
|
96
|
+
</BreadcrumbItem>
|
|
97
|
+
<BreadcrumbSeparator />
|
|
98
|
+
|
|
99
|
+
{isMobile ? renderDrawer() : (
|
|
100
|
+
<>
|
|
101
|
+
{items.slice(0, -1).map((item, index) => {
|
|
102
|
+
const isLast = index === items.length - 1;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Fragment key={`${item.label}-fragment`}>
|
|
106
|
+
<BreadcrumbItem key={`${item.label}-item`}>
|
|
107
|
+
{renderLink(!isLast, item)}
|
|
108
|
+
</BreadcrumbItem>
|
|
109
|
+
{!isLast && (
|
|
110
|
+
<BreadcrumbSeparator key={`${item.label}-separator`} />
|
|
111
|
+
)}
|
|
112
|
+
</Fragment>
|
|
113
|
+
);
|
|
114
|
+
})}
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
<BreadcrumbPage>
|
|
119
|
+
{renderLink(false, lastItem)}
|
|
120
|
+
</BreadcrumbPage>
|
|
121
|
+
</BreadcrumbList>
|
|
122
|
+
</BreadcrumbComponent>
|
|
123
|
+
);
|
|
124
|
+
};
|