@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FC } from "react";
|
|
2
|
+
import { useTranslations } from "next-intl";
|
|
2
3
|
import { CommonItemsModel } from "@c-rex/interfaces";
|
|
3
4
|
import { FileStack } from "lucide-react";
|
|
4
5
|
import { FileDownloadDropdown } from "../renditions/file-download";
|
|
@@ -32,6 +33,8 @@ export const InformationUnitSearchResultsCardList: FC<InformationUnitSearchResul
|
|
|
32
33
|
imageFragmentSubjectIds = [],
|
|
33
34
|
descriptionFragmentSubjectIds = [],
|
|
34
35
|
}) => {
|
|
36
|
+
const t = useTranslations();
|
|
37
|
+
|
|
35
38
|
return (
|
|
36
39
|
<div className="flex-1">
|
|
37
40
|
{items.map((item, index) => {
|
|
@@ -115,7 +118,7 @@ export const InformationUnitSearchResultsCardList: FC<InformationUnitSearchResul
|
|
|
115
118
|
</Button>
|
|
116
119
|
</TooltipTrigger>
|
|
117
120
|
<TooltipContent>
|
|
118
|
-
|
|
121
|
+
{t("availableIn")}: {multipleVersions.join(", ")}
|
|
119
122
|
</TooltipContent>
|
|
120
123
|
</Tooltip>
|
|
121
124
|
)}
|
|
@@ -1,96 +1,196 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { FC, useState } from "react";
|
|
3
|
+
import { FC, useEffect, useRef, useState } from "react";
|
|
4
4
|
|
|
5
5
|
import Link from "next/link";
|
|
6
|
-
import { cn } from "@c-rex/utils";
|
|
6
|
+
import { cn, formatDateToLocale } from "@c-rex/utils";
|
|
7
7
|
import { Badge } from "@c-rex/ui/badge";
|
|
8
|
-
import {
|
|
8
|
+
import { useLocale } from "next-intl";
|
|
9
9
|
import { TopicsResponseItem } from "@c-rex/interfaces";
|
|
10
10
|
import { Card } from "@c-rex/ui/card";
|
|
11
11
|
import { useQueryState } from "nuqs";
|
|
12
|
+
import { User } from "lucide-react";
|
|
12
13
|
|
|
13
14
|
interface InformationUnitSearchResultsCardsProps {
|
|
14
15
|
items: TopicsResponseItem[];
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
type CardData = {
|
|
19
|
+
image: string | null;
|
|
20
|
+
description: string | null;
|
|
21
|
+
authors: { name: string; photo: string | null }[];
|
|
22
|
+
loaded: boolean;
|
|
23
|
+
};
|
|
21
24
|
|
|
22
|
-
return (
|
|
23
|
-
<div className="grid gap-6 grid-cols-2">
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
const InformationUnitSearchResultCard: FC<{
|
|
27
|
+
item: TopicsResponseItem;
|
|
28
|
+
index: number;
|
|
29
|
+
query: string | null;
|
|
30
|
+
}> = ({ item, index, query }) => {
|
|
31
|
+
const locale = useLocale();
|
|
32
|
+
const date = formatDateToLocale(item.created!, locale);
|
|
33
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
34
|
+
const [cardData, setCardData] = useState<CardData>({ image: null, description: null, authors: [], loaded: false });
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const el = cardRef.current;
|
|
38
|
+
if (!el) return;
|
|
39
|
+
|
|
40
|
+
const observer = new IntersectionObserver(
|
|
41
|
+
([entry]) => {
|
|
42
|
+
if (!entry.isIntersecting) return;
|
|
43
|
+
observer.disconnect();
|
|
44
|
+
|
|
45
|
+
const fetches: Promise<void>[] = [];
|
|
46
|
+
|
|
47
|
+
if (item.renditionUrl) {
|
|
48
|
+
fetches.push(
|
|
49
|
+
fetch(item.renditionUrl)
|
|
50
|
+
.then(r => r.text())
|
|
51
|
+
.then(html => {
|
|
52
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
53
|
+
setCardData(prev => ({
|
|
54
|
+
...prev,
|
|
55
|
+
image: doc.querySelector(".teaserfig img")?.getAttribute("src") || null,
|
|
56
|
+
description: doc.querySelector(".teaser-p")?.textContent || null,
|
|
57
|
+
}));
|
|
58
|
+
})
|
|
59
|
+
.catch(() => { })
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (item.vcardUrls?.length) {
|
|
64
|
+
fetches.push(
|
|
65
|
+
Promise.all(
|
|
66
|
+
item.vcardUrls.map(url =>
|
|
67
|
+
fetch(url)
|
|
68
|
+
.then(r => r.json())
|
|
69
|
+
.then(vcard => ({ name: vcard.fullName || "", photo: vcard.photos?.[0]?.source || null }))
|
|
70
|
+
.catch(() => null)
|
|
71
|
+
)
|
|
72
|
+
).then(results => {
|
|
73
|
+
setCardData(prev => ({
|
|
74
|
+
...prev,
|
|
75
|
+
authors: results.filter(Boolean) as { name: string; photo: string | null }[],
|
|
76
|
+
}));
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Promise.all(fetches).then(() => {
|
|
82
|
+
setCardData(prev => ({ ...prev, loaded: true }));
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
{ rootMargin: "200px" }
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
observer.observe(el);
|
|
89
|
+
return () => observer.disconnect();
|
|
90
|
+
}, [item.renditionUrl, item.vcardUrls]);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div ref={cardRef} className={cn(index == 0 ? "md:col-span-2" : "")}>
|
|
94
|
+
<Card
|
|
95
|
+
className={cn(
|
|
96
|
+
"c-rex-result-item information-unit-search-results-card relative p-0 gap-0",
|
|
97
|
+
`c-rex-result-${item.type.toLowerCase()}`,
|
|
98
|
+
index == 0
|
|
99
|
+
? "grid grid-cols-1 md:grid-cols-2"
|
|
100
|
+
: "h-full flex flex-col space-y-2",
|
|
101
|
+
item.disabled ? "c-rex-result-item-disabled" : ""
|
|
102
|
+
)}
|
|
103
|
+
>
|
|
104
|
+
<div className={cn(
|
|
105
|
+
"w-full overflow-hidden",
|
|
106
|
+
index == 0 ? "rounded-t-xl md:rounded-t-none md:rounded-tl-xl md:rounded-bl-xl" : "rounded-t-xl"
|
|
107
|
+
)}>
|
|
108
|
+
{cardData.image ? (
|
|
109
|
+
<img
|
|
110
|
+
src={cardData.image}
|
|
111
|
+
alt={item.title}
|
|
112
|
+
className="size-full object-cover object-center"
|
|
113
|
+
style={{ width: "100%", height: index == 0 ? "100%" : "190px" }}
|
|
114
|
+
loading={index == 0 ? "eager" : "lazy"}
|
|
115
|
+
/>
|
|
116
|
+
) : (
|
|
117
|
+
<div
|
|
118
|
+
style={{ height: "190px" }}
|
|
119
|
+
className={cn(
|
|
120
|
+
"w-full bg-gray-100 animate-pulse",
|
|
121
|
+
index == 0 ? "" : "rounded-t-xl"
|
|
122
|
+
)}
|
|
123
|
+
/>
|
|
34
124
|
)}
|
|
35
|
-
>
|
|
36
|
-
|
|
37
|
-
<div className={
|
|
38
|
-
cn(
|
|
39
|
-
"w-full overflow-hidden",
|
|
40
|
-
index == 0 ? "rounded-tl-xl rounded-bl-xl" : "rounded-t-xl"
|
|
41
|
-
)}>
|
|
42
|
-
{item.image && (
|
|
43
|
-
<img
|
|
44
|
-
src={item.image}
|
|
45
|
-
alt={item.title}
|
|
46
|
-
className={cn(
|
|
47
|
-
"size-full object-cover object-center",
|
|
48
|
-
isLoading ? "bg-gray-300 animate-pulse" : "",
|
|
49
|
-
)}
|
|
50
|
-
style={{
|
|
51
|
-
width: "100%", height: index == 0 ? "auto" : "190px"
|
|
52
|
-
}}
|
|
53
|
-
loading={index == 0 ? "eager" : "lazy"}
|
|
54
|
-
onLoad={() => setLoading(false)}
|
|
55
|
-
onError={() => setLoading(false)}
|
|
56
|
-
/>
|
|
57
|
-
)}
|
|
125
|
+
</div>
|
|
58
126
|
|
|
127
|
+
<div className="flex flex-1 flex-col p-4 gap-4 justify-between">
|
|
128
|
+
<div className="w-full flex flex-col items-start gap-4">
|
|
129
|
+
<h2 className="line-clamp-2 font-heading text-2xl">
|
|
130
|
+
{item.title}
|
|
131
|
+
</h2>
|
|
132
|
+
<Badge variant="secondary"> {date} </Badge>
|
|
59
133
|
</div>
|
|
60
134
|
|
|
61
|
-
<div
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
|
|
135
|
+
<div className="flex items-end flex-row">
|
|
136
|
+
{cardData.description ? (
|
|
137
|
+
<p className="m-0 flex-1 text-sm text-muted-foreground">
|
|
138
|
+
{cardData.description}
|
|
139
|
+
</p>
|
|
140
|
+
) : (
|
|
141
|
+
<div className="flex-1 space-y-2">
|
|
142
|
+
<div className="h-3 w-full rounded bg-gray-100 animate-pulse" />
|
|
143
|
+
<div className="h-3 w-4/5 rounded bg-gray-100 animate-pulse" />
|
|
144
|
+
</div>
|
|
65
145
|
)}
|
|
66
|
-
>
|
|
67
|
-
<div className="w-full">
|
|
68
|
-
<h2 className="my-1.5 line-clamp-2 font-heading text-2xl">
|
|
69
|
-
{item.title}
|
|
70
|
-
</h2>
|
|
71
|
-
|
|
72
|
-
{item.type && (
|
|
73
|
-
<Badge variant="secondary">
|
|
74
|
-
{item.type}
|
|
75
|
-
</Badge>
|
|
76
|
-
)}
|
|
77
|
-
</div>
|
|
78
|
-
<div className="mt-4 flex items-end flex-row gap-2">
|
|
79
|
-
<p className="m-0 flex-1 text-sm text-muted-foreground">{item.description?.substring(0, 100)}...</p>
|
|
146
|
+
</div>
|
|
80
147
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
148
|
+
{cardData.authors.length > 0 && (
|
|
149
|
+
<div className="flex flex-wrap gap-4">
|
|
150
|
+
{cardData.authors.map((author, i) => (
|
|
151
|
+
<div key={i} className="flex items-center gap-2">
|
|
84
152
|
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
153
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
154
|
+
{author.photo ? (
|
|
155
|
+
<img
|
|
156
|
+
src={author.photo}
|
|
157
|
+
alt={author.name}
|
|
158
|
+
className="size-6 rounded-full object-cover"
|
|
159
|
+
/>
|
|
160
|
+
) : (
|
|
161
|
+
<div className="size-6 rounded-full bg-muted flex items-center justify-center" >
|
|
162
|
+
<User className="size-4" />
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
<span className="text-xs text-muted-foreground">{author.name}</span>
|
|
166
|
+
</div>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
92
169
|
)}
|
|
93
|
-
</
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{!item.disabled && (
|
|
173
|
+
<Link href={item.link} className="absolute inset-0">
|
|
174
|
+
<span className="sr-only">View article</span>
|
|
175
|
+
</Link>
|
|
176
|
+
)}
|
|
177
|
+
</Card>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const InformationUnitSearchResultsCards: FC<InformationUnitSearchResultsCardsProps> = ({ items }) => {
|
|
183
|
+
const [query] = useQueryState("search");
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
187
|
+
{items.map((item, index) => (
|
|
188
|
+
<InformationUnitSearchResultCard
|
|
189
|
+
key={item.shortId}
|
|
190
|
+
item={item}
|
|
191
|
+
index={index}
|
|
192
|
+
query={query}
|
|
193
|
+
/>
|
|
94
194
|
))}
|
|
95
195
|
</div>
|
|
96
196
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { FC
|
|
3
|
+
import { FC } from "react";
|
|
4
4
|
import {
|
|
5
5
|
Pagination as PaginationUI,
|
|
6
6
|
PaginationContent,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
PaginationNext,
|
|
11
11
|
PaginationPrevious,
|
|
12
12
|
} from "@c-rex/ui/pagination"
|
|
13
|
-
import {
|
|
13
|
+
import { usePathname, useSearchParams } from "next/navigation";
|
|
14
14
|
import { ResultContainerPageInfoModel } from "@c-rex/interfaces";
|
|
15
15
|
import { useTranslations } from "next-intl";
|
|
16
16
|
import { cn } from "@c-rex/utils";
|
|
@@ -22,6 +22,30 @@ interface PaginationProps {
|
|
|
22
22
|
|
|
23
23
|
type PaginationToken = number | "ellipsis-left" | "ellipsis-right";
|
|
24
24
|
|
|
25
|
+
const isValidPageNumber = (pageNumber: number, currentPage: number, pageCount: number) => (
|
|
26
|
+
pageNumber >= 1
|
|
27
|
+
&& pageNumber <= pageCount
|
|
28
|
+
&& pageNumber !== currentPage
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export const buildPageHref = (
|
|
32
|
+
pathname: string,
|
|
33
|
+
currentSearch: string,
|
|
34
|
+
pageNumber: number,
|
|
35
|
+
currentPage: number,
|
|
36
|
+
pageCount: number,
|
|
37
|
+
) => {
|
|
38
|
+
if (!isValidPageNumber(pageNumber, currentPage, pageCount)) {
|
|
39
|
+
return `${pathname}${currentSearch}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const searchParams = new URLSearchParams(currentSearch);
|
|
43
|
+
searchParams.set("page", String(pageNumber));
|
|
44
|
+
|
|
45
|
+
const queryString = searchParams.toString();
|
|
46
|
+
return queryString.length > 0 ? `${pathname}?${queryString}` : pathname;
|
|
47
|
+
};
|
|
48
|
+
|
|
25
49
|
const buildPaginationTokens = (currentPage: number, pageCount: number): PaginationToken[] => {
|
|
26
50
|
if (pageCount <= 7) {
|
|
27
51
|
return Array.from({ length: pageCount }, (_, index) => index + 1);
|
|
@@ -41,27 +65,8 @@ const buildPaginationTokens = (currentPage: number, pageCount: number): Paginati
|
|
|
41
65
|
export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
|
|
42
66
|
const disabledClass = "opacity-50 pointer-events-none";
|
|
43
67
|
const t = useTranslations("results");
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
parseAsInteger.withOptions({
|
|
47
|
-
history: 'push',
|
|
48
|
-
shallow: false,
|
|
49
|
-
})
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
const onChangePage = (event: MouseEvent<HTMLAnchorElement>, pageNumber: number) => {
|
|
53
|
-
event.preventDefault();
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
pageNumber < 1
|
|
57
|
-
|| (pageInfo.pageCount !== undefined && pageNumber > pageInfo.pageCount)
|
|
58
|
-
|| pageNumber === pageInfo.pageNumber
|
|
59
|
-
) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
void setPage(pageNumber);
|
|
64
|
-
}
|
|
68
|
+
const pathname = usePathname();
|
|
69
|
+
const searchParams = useSearchParams();
|
|
65
70
|
|
|
66
71
|
const pageNumber = pageInfo.pageNumber || 1;
|
|
67
72
|
const pageCount = pageInfo.pageCount || 1;
|
|
@@ -69,32 +74,25 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
|
|
|
69
74
|
const firstItemOnPage = pageInfo.firstItemOnPage || 0;
|
|
70
75
|
const lastItemOnPage = pageInfo.lastItemOnPage || 0;
|
|
71
76
|
const tokens = buildPaginationTokens(pageNumber, pageCount);
|
|
77
|
+
const currentSearch = searchParams.toString();
|
|
78
|
+
const search = currentSearch.length > 0 ? `?${currentSearch}` : "";
|
|
72
79
|
|
|
73
80
|
return (
|
|
74
81
|
<div className={cn("flex flex-col gap-3 py-4", className)}>
|
|
75
|
-
<
|
|
76
|
-
<span>
|
|
82
|
+
<PaginationUI className="py-4 items-center justify-center sm:justify-between">
|
|
83
|
+
<span className="hidden sm:block text-sm text-muted-foreground">
|
|
77
84
|
{t("paginationResults", {
|
|
78
85
|
first: firstItemOnPage,
|
|
79
86
|
last: lastItemOnPage,
|
|
80
87
|
total: totalItemCount,
|
|
81
88
|
})}
|
|
82
89
|
</span>
|
|
83
|
-
<span>
|
|
84
|
-
{t("paginationPage", {
|
|
85
|
-
page: pageNumber,
|
|
86
|
-
pageCount,
|
|
87
|
-
})}
|
|
88
|
-
</span>
|
|
89
|
-
</div>
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
<PaginationContent className="justify-start">
|
|
91
|
+
<PaginationContent className="justify-between sm:justify-start w-full sm:w-auto">
|
|
93
92
|
<PaginationItem>
|
|
94
93
|
<PaginationPrevious
|
|
95
|
-
href=
|
|
94
|
+
href={buildPageHref(pathname, search, pageNumber - 1, pageNumber, pageCount)}
|
|
96
95
|
className={pageNumber === 1 ? disabledClass : ""}
|
|
97
|
-
onClick={(event) => onChangePage(event, pageNumber - 1)}
|
|
98
96
|
/>
|
|
99
97
|
</PaginationItem>
|
|
100
98
|
|
|
@@ -102,9 +100,8 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
|
|
|
102
100
|
typeof token === "number" ? (
|
|
103
101
|
<PaginationItem key={token}>
|
|
104
102
|
<PaginationLink
|
|
105
|
-
href=
|
|
103
|
+
href={buildPageHref(pathname, search, token, pageNumber, pageCount)}
|
|
106
104
|
isActive={token === pageNumber}
|
|
107
|
-
onClick={(event) => onChangePage(event, token)}
|
|
108
105
|
>
|
|
109
106
|
{token}
|
|
110
107
|
</PaginationLink>
|
|
@@ -118,12 +115,18 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
|
|
|
118
115
|
|
|
119
116
|
<PaginationItem>
|
|
120
117
|
<PaginationNext
|
|
121
|
-
href=
|
|
122
|
-
onClick={(event) => onChangePage(event, pageNumber + 1)}
|
|
118
|
+
href={buildPageHref(pathname, search, pageNumber + 1, pageNumber, pageCount)}
|
|
123
119
|
className={pageNumber === pageCount ? disabledClass : ""}
|
|
124
120
|
/>
|
|
125
121
|
</PaginationItem>
|
|
126
122
|
</PaginationContent>
|
|
123
|
+
|
|
124
|
+
<span className="hidden sm:block text-sm text-muted-foreground">
|
|
125
|
+
{t("paginationPage", {
|
|
126
|
+
page: pageNumber,
|
|
127
|
+
pageCount,
|
|
128
|
+
})}
|
|
129
|
+
</span>
|
|
127
130
|
</PaginationUI>
|
|
128
131
|
</div>
|
|
129
132
|
)
|
package/src/search-input.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { FileCheck, FileX, Search } from "lucide-react";
|
|
|
5
5
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@c-rex/ui/tooltip";
|
|
6
6
|
import * as AutocompleteOptions from "./generated/suggestions";
|
|
7
7
|
import { useQueryState } from "nuqs";
|
|
8
|
+
import { useTranslations } from "next-intl";
|
|
8
9
|
|
|
9
10
|
type PlacedOn = "NAVBAR" | "BODY"
|
|
10
11
|
type Props = {
|
|
@@ -24,6 +25,7 @@ export const SearchInput: FC<Props> = ({
|
|
|
24
25
|
alternativeAutocompleteType,
|
|
25
26
|
alternativeOnSelectPath
|
|
26
27
|
}) => {
|
|
28
|
+
const t = useTranslations();
|
|
27
29
|
const [pkg] = useQueryState("package");
|
|
28
30
|
const [checked, setChecked] = useState<boolean>(true);
|
|
29
31
|
const [autocompleteComponentName, setAutocompleteComponentName] = useState<keyof typeof AutocompleteOptions>(autocompleteType);
|
|
@@ -61,11 +63,11 @@ export const SearchInput: FC<Props> = ({
|
|
|
61
63
|
</TooltipTrigger>
|
|
62
64
|
|
|
63
65
|
<TooltipContent>
|
|
64
|
-
|
|
66
|
+
{t("searchInDocument")}
|
|
65
67
|
</TooltipContent>
|
|
66
68
|
</Tooltip>
|
|
67
69
|
</TooltipProvider>
|
|
68
70
|
</>}
|
|
69
71
|
</>
|
|
70
72
|
);
|
|
71
|
-
};
|
|
73
|
+
};
|
|
@@ -46,7 +46,7 @@ export const TocBrowseControls = ({
|
|
|
46
46
|
>
|
|
47
47
|
<div className={cn("min-w-0", justify === "between" ? "flex-1" : "flex-none")}>
|
|
48
48
|
{previous ? (
|
|
49
|
-
<Button asChild variant="ghost"
|
|
49
|
+
<Button asChild variant="ghost" size="icon" className={buttonClassName}>
|
|
50
50
|
<Link href={previous.href} title={previous.label} className={cn("inline-flex min-w-0 items-center", buttonGapClassName)}>
|
|
51
51
|
<ChevronLeft className="h-4 w-4 shrink-0" />
|
|
52
52
|
{showLabels ? <span className="truncate">{previous.label}</span> : null}
|
|
@@ -59,7 +59,7 @@ export const TocBrowseControls = ({
|
|
|
59
59
|
|
|
60
60
|
<div className={cn("min-w-0 text-right", justify === "between" ? "flex-1" : "flex-none")}>
|
|
61
61
|
{next ? (
|
|
62
|
-
<Button asChild variant="ghost"
|
|
62
|
+
<Button asChild variant="ghost" size="icon" className={buttonClassName}>
|
|
63
63
|
<Link href={next.href} title={next.label} className={cn("inline-flex min-w-0 items-center", buttonGapClassName)}>
|
|
64
64
|
{showLabels ? <span className="truncate">{next.label}</span> : null}
|
|
65
65
|
<ChevronRight className="h-4 w-4 shrink-0" />
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useRef, useState, type CSSProperties, type FC, type ReactNode } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
5
6
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
6
7
|
import {
|
|
7
8
|
Sidebar,
|
|
@@ -35,16 +36,6 @@ const DEFAULT_TOC_WIDTH_PX = 336;
|
|
|
35
36
|
const MIN_TOC_WIDTH_PX = 260;
|
|
36
37
|
const MAX_TOC_WIDTH_PX = 640;
|
|
37
38
|
|
|
38
|
-
const loadingSkeleton = (
|
|
39
|
-
<div className="pt-4 space-y-2">
|
|
40
|
-
<Skeleton className="w-auto h-10 mb-2" />
|
|
41
|
-
<Skeleton className="w-auto h-10 mb-2" />
|
|
42
|
-
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
43
|
-
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
44
|
-
<div className="px-2 pt-1 text-xs text-muted-foreground">Inhaltsverzeichnis wird geladen...</div>
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
|
|
48
39
|
export const TocTreePanel: FC<TocTreePanelProps> = ({
|
|
49
40
|
lang,
|
|
50
41
|
rootNodeId,
|
|
@@ -52,13 +43,25 @@ export const TocTreePanel: FC<TocTreePanelProps> = ({
|
|
|
52
43
|
initialChildrenByParentId = {},
|
|
53
44
|
placeholder,
|
|
54
45
|
headerContent,
|
|
55
|
-
headerTitle
|
|
46
|
+
headerTitle,
|
|
56
47
|
loadChildren,
|
|
57
48
|
buildNodeHref,
|
|
58
49
|
defaultWidthPx = DEFAULT_TOC_WIDTH_PX,
|
|
59
50
|
minWidthPx = MIN_TOC_WIDTH_PX,
|
|
60
51
|
maxWidthPx = MAX_TOC_WIDTH_PX,
|
|
61
52
|
}) => {
|
|
53
|
+
const t = useTranslations();
|
|
54
|
+
const resolvedHeaderTitle = headerTitle ?? t("tableOfContent");
|
|
55
|
+
const loadingSkeleton = (
|
|
56
|
+
<div className="pt-4 space-y-2">
|
|
57
|
+
<Skeleton className="w-auto h-10 mb-2" />
|
|
58
|
+
<Skeleton className="w-auto h-10 mb-2" />
|
|
59
|
+
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
60
|
+
<Skeleton className="w-auto h-10 mb-2 ml-8" />
|
|
61
|
+
<div className="px-2 pt-1 text-xs text-muted-foreground">{t("loadingTableOfContents")}</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
|
|
62
65
|
const [childrenByParentId, setChildrenByParentId] = useState<Record<string, TocNodeSummary[]>>(initialChildrenByParentId);
|
|
63
66
|
const [loadingParentIds, setLoadingParentIds] = useState<Set<string>>(new Set());
|
|
64
67
|
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set(activePathIds));
|
|
@@ -184,7 +187,7 @@ export const TocTreePanel: FC<TocTreePanelProps> = ({
|
|
|
184
187
|
{node.hasChildren ? (
|
|
185
188
|
<button
|
|
186
189
|
type="button"
|
|
187
|
-
aria-label={isExpanded ? "
|
|
190
|
+
aria-label={isExpanded ? t("collapseSection") : t("expandSection")}
|
|
188
191
|
className="h-8 w-8 inline-flex items-center justify-center text-muted-foreground hover:text-foreground shrink-0"
|
|
189
192
|
onClick={() => toggleExpanded(node.id, node.hasChildren)}
|
|
190
193
|
>
|
|
@@ -237,8 +240,8 @@ export const TocTreePanel: FC<TocTreePanelProps> = ({
|
|
|
237
240
|
<Sidebar side="left">
|
|
238
241
|
<SidebarHeader className="border-b px-2 py-2">
|
|
239
242
|
<div className="flex min-w-0 w-full items-center justify-between gap-2">
|
|
240
|
-
<div className="min-w-0 overflow-hidden pl-9 text-sm font-medium text-foreground" title={typeof
|
|
241
|
-
<span className="truncate">{
|
|
243
|
+
<div className="min-w-0 overflow-hidden pl-9 text-sm font-medium text-foreground" title={typeof resolvedHeaderTitle === "string" ? resolvedHeaderTitle : undefined}>
|
|
244
|
+
<span className="truncate">{resolvedHeaderTitle}</span>
|
|
242
245
|
</div>
|
|
243
246
|
<div className="ml-auto flex shrink-0 items-center">
|
|
244
247
|
{headerContent}
|
|
@@ -263,8 +266,8 @@ export const TocTreePanel: FC<TocTreePanelProps> = ({
|
|
|
263
266
|
</Sidebar>
|
|
264
267
|
<button
|
|
265
268
|
type="button"
|
|
266
|
-
aria-label="
|
|
267
|
-
title="
|
|
269
|
+
aria-label={t("resizeTableOfContents")}
|
|
270
|
+
title={t("resizeTableOfContents")}
|
|
268
271
|
className="absolute inset-y-0 -right-2 z-20 hidden w-4 cursor-ew-resize lg:block"
|
|
269
272
|
onMouseDown={(event) => {
|
|
270
273
|
resizeStartRef.current = { startX: event.clientX, startWidth: tocWidthPx };
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { FC } from "react";
|
|
2
|
-
import { RenderArticle } from "../render-article";
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
articleHtml: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const ArticleContent: FC<Props> = async ({ articleHtml }) => {
|
|
9
|
-
return (
|
|
10
|
-
<div className="relative flex flex-col gap-4 md:flex-row md:items-start">
|
|
11
|
-
<div className="relative min-w-0 flex-1 md:pr-8">
|
|
12
|
-
<RenderArticle htmlContent={articleHtml} contentLang="" />
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
</div>
|
|
18
|
-
)
|
|
19
|
-
}
|