@c-rex/components 0.1.33 → 0.1.37
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 +1 -1
- package/src/article/article-content.tsx +26 -35
- package/src/autocomplete.tsx +83 -39
- package/src/carousel/carousel.tsx +49 -49
- package/src/favorites/bookmark-button.analysis.md +7 -5
- package/src/favorites/bookmark-button.tsx +37 -39
- package/src/favorites/favorite-button.analysis.md +4 -3
- package/src/favorites/favorite-button.tsx +7 -30
- package/src/navbar/navbar.tsx +11 -6
- package/src/renditions/html.tsx +38 -31
- package/src/restriction-menu/restriction-menu-item.tsx +8 -5
- package/src/restriction-menu/restriction-menu.tsx +24 -22
- package/src/results/pagination.tsx +18 -8
- package/src/search-input.tsx +1 -1
- package/src/stores/favorites-store.ts +21 -21
- /package/src/results/{cards.analysis.md → analysis/cards.analysis.md} +0 -0
- /package/src/results/{dialog-filter.analysis.md → analysis/dialog-filter.analysis.md} +0 -0
- /package/src/results/{empty.analysis.md → analysis/empty.analysis.md} +0 -0
- /package/src/results/{filter-navbar.analysis.md → analysis/filter-navbar.analysis.md} +0 -0
- /package/src/results/{pagination.analysis.md → analysis/pagination.analysis.md} +0 -0
- /package/src/results/{table-with-images.analysis.md → analysis/table-with-images.analysis.md} +0 -0
- /package/src/results/{table.analysis.md → analysis/table.analysis.md} +0 -0
package/package.json
CHANGED
|
@@ -3,53 +3,44 @@ import { RenditionModel } from "@c-rex/interfaces";
|
|
|
3
3
|
import { RenderArticle } from "../render-article";
|
|
4
4
|
import { ArticleActionBar } from "./article-action-bar";
|
|
5
5
|
import * as cheerio from "cheerio"
|
|
6
|
+
import { HtmlRendition } from "../renditions/html";
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
renditions: RenditionModel[] | null | undefined;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export const ArticleContent: FC<Props> = async ({ renditions }) => {
|
|
12
|
-
const empty = (
|
|
13
|
-
<div>No HTML Rendition Available</div>
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
if (renditions == null || renditions.length == 0) return empty;
|
|
17
|
-
|
|
18
|
-
const filteredRenditions = renditions.filter((item) => item.format == "application/xhtml+xml");
|
|
19
|
-
|
|
20
|
-
if (filteredRenditions.length == 0 || filteredRenditions[0] == undefined || filteredRenditions[0].links == undefined) return empty;
|
|
21
|
-
|
|
22
|
-
const filteredLinks = filteredRenditions[0].links.filter((item) => item.rel == "view");
|
|
23
13
|
|
|
24
|
-
|
|
14
|
+
const articleRender = (html: string) => {
|
|
25
15
|
|
|
26
|
-
|
|
27
|
-
const html = await fetch(url, {
|
|
28
|
-
method: "GET",
|
|
29
|
-
headers: {
|
|
30
|
-
Accept: "application/xhtml+xml",
|
|
31
|
-
},
|
|
32
|
-
}).then(res => res.text());
|
|
16
|
+
const $ = cheerio.load(html)
|
|
33
17
|
|
|
34
|
-
|
|
18
|
+
const metaTags = $("meta").map((_, el) => {
|
|
19
|
+
const name = $(el).attr("name")
|
|
20
|
+
const content = $(el).attr("content")
|
|
21
|
+
return name && content ? { name, content } : null
|
|
22
|
+
}).get().filter(Boolean)
|
|
35
23
|
|
|
36
|
-
|
|
37
|
-
const name = $(el).attr("name")
|
|
38
|
-
const content = $(el).attr("content")
|
|
39
|
-
return name && content ? { name, content } : null
|
|
40
|
-
}).get().filter(Boolean)
|
|
24
|
+
const articleHtml = $("main").html() || ""
|
|
41
25
|
|
|
42
|
-
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
{metaTags.map((tag) => (
|
|
29
|
+
<meta key={`${tag.name}-${tag.content}`} name={tag.name} content={tag.content} />
|
|
30
|
+
))}
|
|
31
|
+
<div className="pr-4 relative">
|
|
32
|
+
<RenderArticle htmlContent={articleHtml} contentLang="" />
|
|
33
|
+
</div>
|
|
34
|
+
<ArticleActionBar />
|
|
35
|
+
</>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
43
38
|
|
|
44
39
|
return (
|
|
45
|
-
|
|
46
|
-
{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<RenderArticle htmlContent={articleHtml} contentLang="" />
|
|
51
|
-
</div>
|
|
52
|
-
<ArticleActionBar />
|
|
53
|
-
</>
|
|
40
|
+
<HtmlRendition
|
|
41
|
+
renditions={renditions}
|
|
42
|
+
render={articleRender}
|
|
43
|
+
shortId=""
|
|
44
|
+
/>
|
|
54
45
|
)
|
|
55
46
|
}
|
package/src/autocomplete.tsx
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@c-rex/ui/input-group";
|
|
5
5
|
import { useTranslations } from "next-intl";
|
|
6
6
|
import { SuggestionQueryParams } from "@c-rex/interfaces";
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import { useRouter } from 'next/navigation';
|
|
7
|
+
import { X } from "lucide-react";
|
|
8
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
10
9
|
import { suggestionRequest } from "./generated/create-suggestions-request";
|
|
11
10
|
import { useQueryState } from "nuqs";
|
|
11
|
+
import { useSearchSettingsStore } from "./stores/search-settings-store";
|
|
12
12
|
|
|
13
13
|
export type AutoCompleteProps = {
|
|
14
14
|
initialValue?: string;
|
|
@@ -18,11 +18,13 @@ export type AutoCompleteProps = {
|
|
|
18
18
|
onSelectParams?: { key: string, value: string }[];
|
|
19
19
|
onSelectPath: string
|
|
20
20
|
inputClass?: string
|
|
21
|
+
keepParams?: boolean;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export const AutoComplete = ({
|
|
24
25
|
initialValue = "",
|
|
25
26
|
embedded = true,
|
|
27
|
+
keepParams = false,
|
|
26
28
|
endpoint,
|
|
27
29
|
queryParams,
|
|
28
30
|
onSelectParams,
|
|
@@ -33,7 +35,9 @@ export const AutoComplete = ({
|
|
|
33
35
|
const [pkg] = useQueryState("package");
|
|
34
36
|
|
|
35
37
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
38
|
+
const searchParams = useSearchParams();
|
|
36
39
|
const router = useRouter();
|
|
40
|
+
|
|
37
41
|
const [open, setOpen] = useState(false);
|
|
38
42
|
const [query, setQuery] = useState(initialValue);
|
|
39
43
|
const [loading, setLoading] = useState(false);
|
|
@@ -41,40 +45,62 @@ export const AutoComplete = ({
|
|
|
41
45
|
|
|
42
46
|
const fetchSuggestions = useCallback(async (prefix: string): Promise<string[]> => {
|
|
43
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;
|
|
44
62
|
if (pkg != null) params.scopes = pkg as unknown as string[];
|
|
45
63
|
|
|
46
64
|
const results = await suggestionRequest({ endpoint, prefix, queryParams: params });
|
|
47
65
|
|
|
48
66
|
return results.data;
|
|
49
|
-
}, [endpoint, queryParams]);
|
|
67
|
+
}, [endpoint, pkg, queryParams, searchParams]);
|
|
50
68
|
|
|
51
69
|
const handleSelect = (value: string) => {
|
|
52
70
|
setQuery(value);
|
|
53
71
|
setOpen(false);
|
|
54
|
-
onSelect(value);
|
|
55
|
-
};
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const searchSettings = useSearchSettingsStore.getState();
|
|
73
|
+
const nextParams = new URLSearchParams(keepParams ? searchParams.toString() : "");
|
|
74
|
+
nextParams.set("page", "1");
|
|
60
75
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
params.set("like", searchSettings.like.toString())
|
|
76
|
+
if (value.length > 0) {
|
|
77
|
+
nextParams.set("search", value);
|
|
78
|
+
} else {
|
|
79
|
+
nextParams.delete("search");
|
|
80
|
+
};
|
|
67
81
|
|
|
68
82
|
onSelectParams?.forEach(param => {
|
|
69
|
-
|
|
83
|
+
nextParams.set(param.key, param.value);
|
|
70
84
|
});
|
|
71
85
|
|
|
72
|
-
router.push(`${onSelectPath}?${
|
|
86
|
+
router.push(`${onSelectPath}?${nextParams.toString()}`);
|
|
73
87
|
};
|
|
74
88
|
|
|
75
|
-
|
|
76
|
-
setQuery(
|
|
77
|
-
|
|
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]);
|
|
78
104
|
|
|
79
105
|
useEffect(() => {
|
|
80
106
|
const handleClickOutside = (e: MouseEvent) => {
|
|
@@ -105,23 +131,37 @@ export const AutoComplete = ({
|
|
|
105
131
|
|
|
106
132
|
return (
|
|
107
133
|
<div className="relative flex-1" ref={containerRef}>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
e.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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>
|
|
125
165
|
|
|
126
166
|
{open && (
|
|
127
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">
|
|
@@ -139,7 +179,11 @@ export const AutoComplete = ({
|
|
|
139
179
|
<li
|
|
140
180
|
key={option}
|
|
141
181
|
className="px-4 py-2 hover:bg-accent cursor-pointer text-sm"
|
|
142
|
-
onClick={() =>
|
|
182
|
+
onClick={() => {
|
|
183
|
+
//handleSelect(`"${option}"`)
|
|
184
|
+
// TODO: check if the quotes are necessary
|
|
185
|
+
handleSelect(option)
|
|
186
|
+
}}
|
|
143
187
|
>
|
|
144
188
|
{option}
|
|
145
189
|
</li>
|
|
@@ -22,14 +22,33 @@ import { Badge } from "@c-rex/ui/badge";
|
|
|
22
22
|
import { Flag } from "../icons/flag-icon"
|
|
23
23
|
import { Empty } from "../results/empty";
|
|
24
24
|
import { useLocale, useTranslations } from "next-intl";
|
|
25
|
+
import Link from "next/link";
|
|
26
|
+
import { useBreakpoint } from "@c-rex/ui/hooks";
|
|
27
|
+
import { DEVICE_OPTIONS } from "@c-rex/constants";
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
type PageInfo = {
|
|
30
|
+
hasNextPage: boolean;
|
|
31
|
+
hasPreviousPage: boolean;
|
|
32
|
+
pageCount: number;
|
|
33
|
+
};
|
|
34
|
+
type Props = {
|
|
35
|
+
className?: string;
|
|
36
|
+
arrows?: boolean;
|
|
37
|
+
autoplay?: boolean;
|
|
38
|
+
autoplaySpeed?: number;
|
|
39
|
+
itemsByRow?: {
|
|
40
|
+
[DEVICE_OPTIONS.MOBILE]: number,
|
|
41
|
+
[DEVICE_OPTIONS.TABLET]: number,
|
|
42
|
+
[DEVICE_OPTIONS.DESKTOP]: number,
|
|
43
|
+
};
|
|
44
|
+
indicators?: boolean;
|
|
45
|
+
showImages?: boolean;
|
|
46
|
+
carouselItemComponent?: FC<{ item: CommonItemsModel; showImages: boolean; linkPattern: string }>;
|
|
47
|
+
serviceType: keyof typeof ServiceOptions;
|
|
48
|
+
queryParams?: Record<string, any>;
|
|
49
|
+
loadByPages?: boolean;
|
|
50
|
+
linkPattern: string;
|
|
31
51
|
};
|
|
32
|
-
|
|
33
52
|
type CarouselContextProps = {
|
|
34
53
|
next: () => void;
|
|
35
54
|
prev: () => void;
|
|
@@ -54,30 +73,15 @@ export function useCarousel() {
|
|
|
54
73
|
return context;
|
|
55
74
|
}
|
|
56
75
|
|
|
57
|
-
type PageInfo = {
|
|
58
|
-
hasNextPage: boolean;
|
|
59
|
-
hasPreviousPage: boolean;
|
|
60
|
-
pageCount: number;
|
|
61
|
-
};
|
|
62
|
-
type Props = {
|
|
63
|
-
className?: string;
|
|
64
|
-
arrows?: boolean;
|
|
65
|
-
autoplay?: boolean;
|
|
66
|
-
autoplaySpeed?: number;
|
|
67
|
-
responsive?: ResponsiveSetting[];
|
|
68
|
-
indicators?: boolean;
|
|
69
|
-
showImages?: boolean;
|
|
70
|
-
carouselItemComponent?: FC<{ item: CommonItemsModel; showImages: boolean }>;
|
|
71
|
-
serviceType: keyof typeof ServiceOptions;
|
|
72
|
-
queryParams?: Record<string, any>;
|
|
73
|
-
loadByPages?: boolean;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
76
|
export const Carousel: FC<Props> = ({
|
|
77
77
|
className,
|
|
78
78
|
arrows = false,
|
|
79
79
|
autoplay = false,
|
|
80
|
-
|
|
80
|
+
itemsByRow = {
|
|
81
|
+
[DEVICE_OPTIONS.MOBILE]: 1,
|
|
82
|
+
[DEVICE_OPTIONS.TABLET]: 3,
|
|
83
|
+
[DEVICE_OPTIONS.DESKTOP]: 4,
|
|
84
|
+
},
|
|
81
85
|
indicators = false,
|
|
82
86
|
autoplaySpeed = 3000,
|
|
83
87
|
showImages = false,
|
|
@@ -85,29 +89,18 @@ export const Carousel: FC<Props> = ({
|
|
|
85
89
|
serviceType,
|
|
86
90
|
queryParams = {},
|
|
87
91
|
loadByPages = false,
|
|
92
|
+
linkPattern
|
|
88
93
|
}) => {
|
|
89
94
|
const service = ServiceOptions[serviceType] as typeof documentsGetAllClientService;
|
|
90
95
|
const RenderComponent = carouselItemComponent || DefaultRenderCarouselItem;
|
|
91
96
|
|
|
92
|
-
|
|
93
|
-
const [slidesToShow, setSlidesToShow] = useState(
|
|
94
|
-
|
|
95
|
-
const width = window.innerWidth;
|
|
96
|
-
const sorted = [...responsive].sort((a, b) => a.maxWidth - b.maxWidth);
|
|
97
|
-
let found = sorted[sorted.length - 1]?.slidesToShow || 1;
|
|
98
|
-
for (const r of sorted) {
|
|
99
|
-
if (width <= r.maxWidth) {
|
|
100
|
-
found = r.slidesToShow;
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
setSlidesToShow(found);
|
|
105
|
-
}, [responsive]);
|
|
97
|
+
const device = useBreakpoint();
|
|
98
|
+
const [slidesToShow, setSlidesToShow] = useState(1);
|
|
99
|
+
|
|
106
100
|
useEffect(() => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}, [updateSlidesToShow]);
|
|
101
|
+
if (device == null) return;
|
|
102
|
+
setSlidesToShow(itemsByRow[device as keyof typeof DEVICE_OPTIONS] as number);
|
|
103
|
+
}, [device, itemsByRow]);
|
|
111
104
|
|
|
112
105
|
// State
|
|
113
106
|
const [current, setCurrent] = useState(0);
|
|
@@ -240,7 +233,7 @@ export const Carousel: FC<Props> = ({
|
|
|
240
233
|
className={`flex-shrink-0 flex-grow-0 flex justify-center`}
|
|
241
234
|
style={{ width: `${100 / slidesToShow}%` }}
|
|
242
235
|
>
|
|
243
|
-
<RenderComponent item={item} showImages={showImages} />
|
|
236
|
+
<RenderComponent item={item} showImages={showImages} linkPattern={linkPattern} />
|
|
244
237
|
</div>
|
|
245
238
|
))
|
|
246
239
|
) : null}
|
|
@@ -258,7 +251,7 @@ const CarouselNext: FC = () => {
|
|
|
258
251
|
const { next, current, slidesToShow, slidesLength, hasNextPage } = useCarousel();
|
|
259
252
|
const disabled = hasNextPage === undefined ? current >= slidesLength - slidesToShow : !hasNextPage;
|
|
260
253
|
return (
|
|
261
|
-
<Button className="
|
|
254
|
+
<Button className="w-9" rounded="full" onClick={next} variant="default" disabled={disabled}>
|
|
262
255
|
<ArrowRight />
|
|
263
256
|
</Button>
|
|
264
257
|
);
|
|
@@ -267,8 +260,9 @@ const CarouselNext: FC = () => {
|
|
|
267
260
|
const CarouselPrev: FC = () => {
|
|
268
261
|
const { prev, current, hasPrevPage } = useCarousel();
|
|
269
262
|
const disabled = hasPrevPage === undefined ? current === 0 : !hasPrevPage;
|
|
263
|
+
|
|
270
264
|
return (
|
|
271
|
-
<Button className="
|
|
265
|
+
<Button className="w-9" rounded="full" onClick={prev} variant="default" disabled={disabled}>
|
|
272
266
|
<ArrowLeft />
|
|
273
267
|
</Button>
|
|
274
268
|
);
|
|
@@ -315,7 +309,11 @@ const CarouselIndicators: FC<{ className?: string }> = ({ className }) => {
|
|
|
315
309
|
);
|
|
316
310
|
};
|
|
317
311
|
|
|
318
|
-
const DefaultRenderCarouselItem: FC<{
|
|
312
|
+
const DefaultRenderCarouselItem: FC<{
|
|
313
|
+
item: CommonItemsModel;
|
|
314
|
+
showImages: boolean;
|
|
315
|
+
linkPattern: string
|
|
316
|
+
}> = ({ item, showImages, linkPattern }) => {
|
|
319
317
|
const locale = useLocale();
|
|
320
318
|
const t = useTranslations("itemTypes");
|
|
321
319
|
|
|
@@ -338,7 +336,9 @@ const DefaultRenderCarouselItem: FC<{ item: CommonItemsModel; showImages: boolea
|
|
|
338
336
|
/>
|
|
339
337
|
)}
|
|
340
338
|
|
|
341
|
-
<
|
|
339
|
+
<Link href={linkPattern.replace("{id}", item.shortId!)} className="hover:underline text-lg font-semibold flex-1">
|
|
340
|
+
{title}
|
|
341
|
+
</Link>
|
|
342
342
|
|
|
343
343
|
<div className="flex justify-between w-full">
|
|
344
344
|
<span className="w-8 block border">
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
- Component: packages/components/src/favorites/bookmark-button.tsx
|
|
4
4
|
- Type: Client component
|
|
5
|
-
- Detected signals: useQueryStates=no, stores=
|
|
5
|
+
- Detected signals: useQueryStates=no, stores=no, window/document=yes, effects=yes, fetch/call=no
|
|
6
6
|
|
|
7
7
|
## Possible re-render causes
|
|
8
|
-
-
|
|
8
|
+
- Effects that update state can add extra renders on mount and dependency changes.
|
|
9
9
|
|
|
10
10
|
## Possible bugs/risks
|
|
11
|
-
-
|
|
11
|
+
- Access to window/document needs care to avoid hydration mismatch and non-browser runtime issues.
|
|
12
|
+
- markersList is loaded only on mount/shortId change; later store updates may not refresh the badge.
|
|
12
13
|
|
|
13
14
|
## Recommended improvements
|
|
14
|
-
- Keep
|
|
15
|
-
-
|
|
15
|
+
- Keep browser-only access inside event handlers or useEffect, not in initial render paths.
|
|
16
|
+
- Read documents[shortId]?.topics directly through a Zustand selector to keep UI reactive.
|
|
17
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { ComponentProps, FC, useState } from "react";
|
|
4
|
-
import { Button } from "@c-rex/ui/button";
|
|
3
|
+
import { ComponentProps, FC, ReactNode, useEffect, useState } from "react";
|
|
4
|
+
import { Button, ButtonProps } from "@c-rex/ui/button";
|
|
5
5
|
import { Trash } from "lucide-react";
|
|
6
6
|
import { cn } from "@c-rex/utils";
|
|
7
7
|
import Link from "next/link";
|
|
@@ -20,18 +20,17 @@ type BookmarkProps = {
|
|
|
20
20
|
triggerVariant?: ComponentProps<typeof Button>["variant"];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const EMPTY_TOPICS: Array<{ id: string; label: string; color: string }> = [];
|
|
24
|
-
|
|
25
23
|
export const BookmarkButton: FC<BookmarkProps> = ({
|
|
26
24
|
shortId,
|
|
27
25
|
triggerVariant = "outline"
|
|
28
26
|
}) => {
|
|
29
|
-
const [
|
|
30
|
-
const document = useFavoritesStore((state) => state.documents[shortId]);
|
|
31
|
-
const markersList = document?.topics ?? EMPTY_TOPICS;
|
|
27
|
+
const [markersList, setMarkersList] = useState<Array<{ id: string; label: string; color: string }>>([]);
|
|
32
28
|
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setMarkersList(useFavoritesStore.getState().documents[shortId]?.topics || []);
|
|
31
|
+
}, [shortId]);
|
|
33
32
|
return (
|
|
34
|
-
<Dialog
|
|
33
|
+
<Dialog>
|
|
35
34
|
<DialogTrigger asChild>
|
|
36
35
|
<Button variant={triggerVariant} size="icon" className="relative">
|
|
37
36
|
<FaRegBookmark className="text-primary" />
|
|
@@ -44,38 +43,37 @@ export const BookmarkButton: FC<BookmarkProps> = ({
|
|
|
44
43
|
</span>
|
|
45
44
|
)}
|
|
46
45
|
</Button>
|
|
46
|
+
|
|
47
47
|
</DialogTrigger>
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
<
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
</DialogContent>
|
|
78
|
-
)}
|
|
48
|
+
<DialogContent>
|
|
49
|
+
<DialogHeader>
|
|
50
|
+
<DialogTitle>Bookmarks</DialogTitle>
|
|
51
|
+
<DialogDescription>
|
|
52
|
+
Manage your bookmarks here
|
|
53
|
+
</DialogDescription>
|
|
54
|
+
</DialogHeader>
|
|
55
|
+
<Table>
|
|
56
|
+
<TableBody>
|
|
57
|
+
{markersList.map((item) => (
|
|
58
|
+
<TableRow key={item.id} className="min-h-12">
|
|
59
|
+
<TableCell>
|
|
60
|
+
<FaRegBookmark className={cn("w-5", `text-${item.color}`)} />
|
|
61
|
+
</TableCell>
|
|
62
|
+
<TableCell>
|
|
63
|
+
<Link href={`${window.location.origin}/topics/${item.id}`}>
|
|
64
|
+
{item.label}
|
|
65
|
+
</Link>
|
|
66
|
+
</TableCell>
|
|
67
|
+
<TableCell>
|
|
68
|
+
<Button variant="destructive" size="icon">
|
|
69
|
+
<Trash className="w-5 hover:text-red-600 cursor-pointer" />
|
|
70
|
+
</Button>
|
|
71
|
+
</TableCell>
|
|
72
|
+
</TableRow>
|
|
73
|
+
))}
|
|
74
|
+
</TableBody>
|
|
75
|
+
</Table>
|
|
76
|
+
</DialogContent>
|
|
79
77
|
</Dialog>
|
|
80
78
|
)
|
|
81
79
|
}
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
## Possible bugs/risks
|
|
12
12
|
- Async flows can hit race conditions during fast param/route changes.
|
|
13
|
-
-
|
|
13
|
+
- useEffect with empty deps calls getTopicDocumentData(id) and does not react if id/type changes.
|
|
14
14
|
|
|
15
15
|
## Recommended improvements
|
|
16
|
-
-
|
|
17
|
-
-
|
|
16
|
+
- Consider cancellation guards (isMounted or AbortController) and standardized error handling.
|
|
17
|
+
- Include id and type in effect dependencies and handle fetch cancellation on unmount.
|
|
18
|
+
|
|
@@ -19,32 +19,10 @@ export const FavoriteButton: FC<{ id: string, type: ResultTypes, label: string }
|
|
|
19
19
|
const [documentData, setDocumentData] = useState<{ id: string, label: string }>({ id, label });
|
|
20
20
|
|
|
21
21
|
useEffect(() => {
|
|
22
|
-
if (type
|
|
23
|
-
|
|
24
|
-
return;
|
|
22
|
+
if (type === RESULT_TYPES.TOPIC) {
|
|
23
|
+
getTopicDocumentData(id);
|
|
25
24
|
}
|
|
26
|
-
|
|
27
|
-
let isCancelled = false;
|
|
28
|
-
const controller = new AbortController();
|
|
29
|
-
|
|
30
|
-
const loadTopicDocumentData = async () => {
|
|
31
|
-
try {
|
|
32
|
-
const data = await getTopicDocumentData(id, controller.signal);
|
|
33
|
-
if (!isCancelled) {
|
|
34
|
-
setDocumentData(data);
|
|
35
|
-
}
|
|
36
|
-
} catch (error) {
|
|
37
|
-
if ((error as Error).name === "AbortError") return;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
loadTopicDocumentData();
|
|
42
|
-
|
|
43
|
-
return () => {
|
|
44
|
-
isCancelled = true;
|
|
45
|
-
controller.abort();
|
|
46
|
-
};
|
|
47
|
-
}, [id, label, type]);
|
|
25
|
+
}, []);
|
|
48
26
|
|
|
49
27
|
const addFavorite = async (id: string) => {
|
|
50
28
|
if (type === RESULT_TYPES.DOCUMENT) {
|
|
@@ -67,18 +45,17 @@ export const FavoriteButton: FC<{ id: string, type: ResultTypes, label: string }
|
|
|
67
45
|
removeFavoriteTopic(documentData.id, id);
|
|
68
46
|
}
|
|
69
47
|
|
|
70
|
-
const getTopicDocumentData = async (topicId: string
|
|
48
|
+
const getTopicDocumentData = async (topicId: string): Promise<void> => {
|
|
71
49
|
|
|
72
50
|
const response = await fetch(`/api/information-units/document-by-topic-id?shortId=${topicId}`, {
|
|
73
|
-
method: "GET"
|
|
74
|
-
signal,
|
|
51
|
+
method: "GET"
|
|
75
52
|
});
|
|
76
53
|
|
|
77
54
|
if (!response.ok) throw new Error("Failed to fetch document by topic id")
|
|
78
55
|
|
|
79
|
-
const { documentId, label
|
|
56
|
+
const { documentId, label } = await response.json();
|
|
80
57
|
|
|
81
|
-
|
|
58
|
+
setDocumentData({ id: documentId, label });
|
|
82
59
|
}
|
|
83
60
|
|
|
84
61
|
if (isFavorite) {
|
package/src/navbar/navbar.tsx
CHANGED
|
@@ -30,7 +30,7 @@ export const NavBar: FC<NavBarProps> = async ({
|
|
|
30
30
|
showInput,
|
|
31
31
|
autocompleteType,
|
|
32
32
|
onSelectPath,
|
|
33
|
-
showMenu =
|
|
33
|
+
showMenu = true,
|
|
34
34
|
...props
|
|
35
35
|
}) => {
|
|
36
36
|
const t = await getTranslations();
|
|
@@ -75,27 +75,32 @@ export const NavBar: FC<NavBarProps> = async ({
|
|
|
75
75
|
</Button>
|
|
76
76
|
}
|
|
77
77
|
>
|
|
78
|
-
<Button asChild variant="link">
|
|
78
|
+
<Button asChild variant="link" className="flex justify-start">
|
|
79
|
+
<Link href="/">
|
|
80
|
+
{t('navigation.home')}
|
|
81
|
+
</Link>
|
|
82
|
+
</Button>
|
|
83
|
+
<Button asChild variant="link" className="flex justify-start">
|
|
79
84
|
<Link href="/documents">
|
|
80
85
|
{t('navigation.documents')}
|
|
81
86
|
</Link>
|
|
82
87
|
</Button>
|
|
83
|
-
<Button asChild variant="link">
|
|
88
|
+
<Button asChild variant="link" className="flex justify-start">
|
|
84
89
|
<Link href="/topics">
|
|
85
90
|
{t('navigation.topics')}
|
|
86
91
|
</Link>
|
|
87
92
|
</Button>
|
|
88
|
-
<Button asChild variant="link">
|
|
93
|
+
<Button asChild variant="link" className="flex justify-start">
|
|
89
94
|
<Link href="/fragments">
|
|
90
95
|
{t('navigation.fragments')}
|
|
91
96
|
</Link>
|
|
92
97
|
</Button>
|
|
93
|
-
<Button asChild variant="link">
|
|
98
|
+
<Button asChild variant="link" className="flex justify-start">
|
|
94
99
|
<Link href="/packages">
|
|
95
100
|
{t('navigation.packages')}
|
|
96
101
|
</Link>
|
|
97
102
|
</Button>
|
|
98
|
-
<Button asChild variant="link">
|
|
103
|
+
<Button asChild variant="link" className="flex justify-start">
|
|
99
104
|
<Link href="/information-units">
|
|
100
105
|
{t('navigation.informationUnits')}
|
|
101
106
|
</Link>
|
package/src/renditions/html.tsx
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { FC, JSX } from "react";
|
|
2
2
|
import * as cheerio from "cheerio"
|
|
3
|
-
import {
|
|
3
|
+
import { RenditionModel } from "@c-rex/interfaces";
|
|
4
|
+
import { fragmentsGetAllServer } from "@c-rex/services/server-requests";
|
|
5
|
+
import { call } from "@c-rex/utils";
|
|
4
6
|
|
|
5
7
|
interface HtmlRenditionProps {
|
|
6
8
|
htmlFormats?: string[]
|
|
7
9
|
shortId: string,
|
|
8
|
-
render?: (html: string) => JSX.Element
|
|
10
|
+
render?: (html: string) => JSX.Element,
|
|
11
|
+
renditions?: RenditionModel[] | null
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
const defaultRender = (html: string) => {
|
|
@@ -14,45 +17,49 @@ const defaultRender = (html: string) => {
|
|
|
14
17
|
return <div dangerouslySetInnerHTML={{ __html: articleHtml }} />;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
export const HtmlRendition: FC<HtmlRenditionProps> = ({
|
|
20
|
+
export const HtmlRendition: FC<HtmlRenditionProps> = async ({
|
|
18
21
|
shortId,
|
|
19
22
|
htmlFormats = ["application/xhtml+xml", "application/html", "text/html"],
|
|
20
|
-
render
|
|
23
|
+
render = defaultRender,
|
|
24
|
+
renditions
|
|
21
25
|
}) => {
|
|
22
26
|
const empty = <div>No rendition available</div>;
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
if (renditions == undefined) {
|
|
29
|
+
const result = await fragmentsGetAllServer({
|
|
30
|
+
Fields: ["titles", "renditions"],
|
|
31
|
+
Embed: ["renditions"],
|
|
32
|
+
PageSize: 1,
|
|
33
|
+
Links: true,
|
|
34
|
+
Restrict: [
|
|
35
|
+
`informationUnits=${shortId}`,
|
|
36
|
+
...htmlFormats.map(format => `renditions.format=${format}`)
|
|
37
|
+
],
|
|
38
|
+
})
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (error) {
|
|
39
|
-
return <div>Error loading content</div>;
|
|
40
|
-
}
|
|
40
|
+
renditions = result.items?.[0]?.renditions;
|
|
41
|
+
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
if (renditions == null || renditions.length == 0) return empty;
|
|
44
|
+
renditions = renditions.filter(rendition => htmlFormats.includes(rendition.format!));
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
if (renditions.length == 0 || renditions[0] == undefined || renditions[0].links == undefined) return empty;
|
|
46
|
+
if (renditions.length == 0 || renditions[0] == undefined || renditions[0].links == undefined) return empty;
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
const filteredLinks = renditions[0].links.filter((item) => item.rel == "view");
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
if (filteredLinks.length == 0 || filteredLinks[0] == undefined || filteredLinks[0].href == undefined) return empty;
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const url = filteredLinks[0].href;
|
|
53
|
+
try {
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const html = await fetch(url).then(res => res.text());
|
|
56
|
+
return render(html);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
call("CrexLogger.log", {
|
|
59
|
+
level: "error",
|
|
60
|
+
message: `HtmlRendition error: ${error}`
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return empty;
|
|
64
|
+
}
|
|
58
65
|
}
|
|
@@ -77,23 +77,26 @@ export const RestrictionDropdownItem: FC<Props> = ({
|
|
|
77
77
|
shortId,
|
|
78
78
|
label,
|
|
79
79
|
restrictField,
|
|
80
|
+
selected = false,
|
|
80
81
|
}) => {
|
|
81
82
|
const [restrict, setRestrict] = useQueryState("restrict", {
|
|
82
83
|
shallow: false,
|
|
83
84
|
history: "push",
|
|
84
85
|
});
|
|
85
86
|
|
|
86
|
-
const { restrictionValue } = getRestrictionValue({
|
|
87
|
+
const { restrictionValue, shouldRemoveRestrictParam } = getRestrictionValue({
|
|
87
88
|
shortId,
|
|
88
89
|
restrictField,
|
|
90
|
+
selected,
|
|
89
91
|
currentRestrict: restrict,
|
|
90
92
|
});
|
|
91
93
|
|
|
92
94
|
return (
|
|
93
95
|
<Button
|
|
94
|
-
variant="ghost"
|
|
95
|
-
onClick={() => setRestrict(restrictionValue)}
|
|
96
|
-
|
|
96
|
+
variant={selected ? "default" : "ghost"}
|
|
97
|
+
onClick={() => shouldRemoveRestrictParam ? setRestrict(null) : setRestrict(restrictionValue)}
|
|
98
|
+
rounded="full"
|
|
99
|
+
className="text-left text-wrap !h-auto min-h-10 w-full !justify-start cursor-pointer"
|
|
97
100
|
>
|
|
98
101
|
{label}
|
|
99
102
|
</Button>
|
|
@@ -141,4 +144,4 @@ function getRestrictionValue({
|
|
|
141
144
|
const restrictionValue = shouldRemoveRestrictParam ? null : restrictParam
|
|
142
145
|
|
|
143
146
|
return { restrictionValue, shouldRemoveRestrictParam };
|
|
144
|
-
}
|
|
147
|
+
}
|
|
@@ -5,10 +5,9 @@ import {
|
|
|
5
5
|
NavigationMenu,
|
|
6
6
|
NavigationMenuList,
|
|
7
7
|
NavigationMenuItem,
|
|
8
|
+
NavigationMenuTrigger,
|
|
9
|
+
NavigationMenuContent,
|
|
8
10
|
} from "@c-rex/ui/navigation-menu";
|
|
9
|
-
import { Button } from "@c-rex/ui/button";
|
|
10
|
-
import { ChevronDown } from "lucide-react";
|
|
11
|
-
import { DropdownHoverItem } from "@c-rex/ui/dropdown-hover-item";
|
|
12
11
|
import { RestrictionDropdownItem, RestrictionNavigationItem } from "./restriction-menu-item";
|
|
13
12
|
import { parseAsString, useQueryStates } from "nuqs";
|
|
14
13
|
import { InformationSubjectModel } from "@c-rex/interfaces";
|
|
@@ -110,13 +109,13 @@ export const RestrictionMenu: FC<Props> = ({
|
|
|
110
109
|
}, [sortedItems, visibleCount]);
|
|
111
110
|
|
|
112
111
|
return (
|
|
113
|
-
<NavigationMenu className="max-w-full w-full c-rex-restriction-menu">
|
|
112
|
+
<NavigationMenu viewport={false} className="max-w-full w-full c-rex-restriction-menu">
|
|
114
113
|
<NavigationMenuList className={cn("w-full", navigationMenuListClassName)}>
|
|
115
114
|
|
|
116
115
|
|
|
117
116
|
<RestrictionNavigationItem
|
|
118
117
|
removeRestrictParam
|
|
119
|
-
label=
|
|
118
|
+
label={t('all')}
|
|
120
119
|
selected={restrictionValues.length === 0}
|
|
121
120
|
/>
|
|
122
121
|
|
|
@@ -131,23 +130,26 @@ export const RestrictionMenu: FC<Props> = ({
|
|
|
131
130
|
))}
|
|
132
131
|
|
|
133
132
|
<NavigationMenuItem>
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
133
|
+
<NavigationMenuTrigger>
|
|
134
|
+
{t('more')}
|
|
135
|
+
</NavigationMenuTrigger>
|
|
136
|
+
<NavigationMenuContent className="w-96 sm:w-[] md:w-[700px] lg:w-[65rem]">
|
|
137
|
+
<ul className="grid gap-1 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4">
|
|
138
|
+
{hiddenItems.map((item) => (
|
|
139
|
+
<li
|
|
140
|
+
key={item.shortId}
|
|
141
|
+
className="flex items-center"
|
|
142
|
+
>
|
|
143
|
+
<RestrictionDropdownItem
|
|
144
|
+
shortId={item.shortId!}
|
|
145
|
+
restrictField={restrictField as string}
|
|
146
|
+
label={getLabelByLang(item.labels, lang)}
|
|
147
|
+
selected={restrictionValues.includes(item.shortId!)}
|
|
148
|
+
/>
|
|
149
|
+
</li>
|
|
150
|
+
))}
|
|
151
|
+
</ul>
|
|
152
|
+
</NavigationMenuContent>
|
|
151
153
|
</NavigationMenuItem>
|
|
152
154
|
</NavigationMenuList>
|
|
153
155
|
</NavigationMenu>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { FC } from "react";
|
|
3
|
+
import { FC, MouseEvent } from "react";
|
|
4
4
|
import {
|
|
5
5
|
Pagination as PaginationUI,
|
|
6
6
|
PaginationContent,
|
|
@@ -19,15 +19,25 @@ interface PaginationProps {
|
|
|
19
19
|
export const Pagination: FC<PaginationProps> = ({ pageInfo }) => {
|
|
20
20
|
const disabledClass = "opacity-50 pointer-events-none";
|
|
21
21
|
|
|
22
|
-
const [
|
|
22
|
+
const [, setPage] = useQueryState('page',
|
|
23
23
|
parseAsInteger.withOptions({
|
|
24
24
|
history: 'push',
|
|
25
25
|
shallow: false,
|
|
26
26
|
})
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
-
const onChangePage = (pageNumber: number) => {
|
|
30
|
-
|
|
29
|
+
const onChangePage = (event: MouseEvent<HTMLAnchorElement>, pageNumber: number) => {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
pageNumber < 1
|
|
34
|
+
|| (pageInfo.pageCount !== undefined && pageNumber > pageInfo.pageCount)
|
|
35
|
+
|| pageNumber === pageInfo.pageNumber
|
|
36
|
+
) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
void setPage(pageNumber);
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
return (
|
|
@@ -37,7 +47,7 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo }) => {
|
|
|
37
47
|
<PaginationPrevious
|
|
38
48
|
href="#"
|
|
39
49
|
className={pageInfo.pageNumber === 1 ? disabledClass : ""}
|
|
40
|
-
onClick={() => onChangePage(pageInfo.pageNumber! - 1)}
|
|
50
|
+
onClick={(event) => onChangePage(event, pageInfo.pageNumber! - 1)}
|
|
41
51
|
/>
|
|
42
52
|
</PaginationItem>
|
|
43
53
|
|
|
@@ -46,7 +56,7 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo }) => {
|
|
|
46
56
|
<PaginationLink
|
|
47
57
|
href="#"
|
|
48
58
|
isActive={page === pageInfo.pageNumber}
|
|
49
|
-
onClick={() => onChangePage(page)}
|
|
59
|
+
onClick={(event) => onChangePage(event, page)}
|
|
50
60
|
>
|
|
51
61
|
{page}
|
|
52
62
|
</PaginationLink>
|
|
@@ -61,11 +71,11 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo }) => {
|
|
|
61
71
|
<PaginationItem>
|
|
62
72
|
<PaginationNext
|
|
63
73
|
href="#"
|
|
64
|
-
onClick={() => onChangePage(pageInfo.pageNumber! + 1)}
|
|
74
|
+
onClick={(event) => onChangePage(event, pageInfo.pageNumber! + 1)}
|
|
65
75
|
className={pageInfo.pageNumber === pageInfo.pageCount ? disabledClass : ""}
|
|
66
76
|
/>
|
|
67
77
|
</PaginationItem>
|
|
68
78
|
</PaginationContent>
|
|
69
79
|
</PaginationUI>
|
|
70
80
|
)
|
|
71
|
-
}
|
|
81
|
+
}
|
package/src/search-input.tsx
CHANGED
|
@@ -44,7 +44,7 @@ export const SearchInput: FC<Props> = ({
|
|
|
44
44
|
<>
|
|
45
45
|
<Search className="shrink-0 opacity-50" />
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
<AutocompleteComponent
|
|
49
49
|
onSelectParams={
|
|
50
50
|
(pkg && checked && showPkgFilter) ? [{ key: "packages", value: pkg }] : []
|
|
@@ -60,29 +60,29 @@ export const useFavoritesStore = create<FavoritesStore>()(
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
const favoriteTopic = (documents: Record<string, { topics: Favorite[] }>, documentId: string, id: string, label: string, color: string): Record<string, { topics: Favorite[] }> => {
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
[documentId]:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
|
|
64
|
+
const documentsCopy = { ...documents };
|
|
65
|
+
const notFound = documents[documentId] == undefined;
|
|
66
|
+
|
|
67
|
+
if (notFound) {
|
|
68
|
+
documentsCopy[documentId] = { topics: [] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
documentsCopy[documentId]!.topics.push({ id, label, color });
|
|
72
|
+
|
|
73
|
+
return documentsCopy
|
|
73
74
|
};
|
|
74
75
|
|
|
75
76
|
const unfavoriteTopic = (documents: Record<string, { topics: Favorite[] }>, documentId: string, id: string): Record<string, { topics: Favorite[] }> => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
|
|
78
|
+
const documentsCopy = { ...documents };
|
|
79
|
+
const notFound = documents[documentId] == undefined;
|
|
80
|
+
|
|
81
|
+
if (notFound) {
|
|
82
|
+
return documentsCopy;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
topics: currentDocument.topics.filter((topic) => topic.id !== id),
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
85
|
+
documentsCopy[documentId]!.topics = documentsCopy[documentId]!.topics.filter(topic => topic.id !== id);
|
|
86
|
+
|
|
87
|
+
return documentsCopy
|
|
88
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/src/results/{table-with-images.analysis.md → analysis/table-with-images.analysis.md}
RENAMED
|
File without changes
|
|
File without changes
|