@c-rex/components 0.1.36 → 0.1.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 +1 -18
- package/src/article/article-content.tsx +26 -35
- package/src/autocomplete.tsx +26 -5
- package/src/carousel/carousel.tsx +53 -52
- package/src/check-article-lang.tsx +8 -4
- package/src/favorites/bookmark-button.tsx +26 -11
- package/src/favorites/favorite-button.tsx +67 -21
- package/src/info/info-table.tsx +60 -38
- package/src/info/set-available-versions.tsx +19 -0
- package/src/navbar/language-switcher/content-language-switch.tsx +36 -36
- package/src/navbar/language-switcher/ui-language-switch.tsx +5 -6
- package/src/navbar/navbar.tsx +17 -8
- package/src/renditions/html.tsx +38 -31
- package/src/restriction-menu/restriction-menu-item.tsx +8 -5
- package/src/restriction-menu/restriction-menu.tsx +26 -25
- package/src/results/generic/table-result-list.tsx +1 -1
- package/src/results/utils.ts +9 -2
- package/src/stores/favorites-store.ts +21 -21
- package/src/stores/language-store.ts +2 -31
- package/src/article/article-action-bar.analysis.md +0 -15
- package/src/article/article-action-bar.stories.tsx +0 -15
- package/src/article/article-content.analysis.md +0 -15
- package/src/article/article-content.stories.tsx +0 -21
- package/src/autocomplete.analysis.md +0 -17
- package/src/breadcrumb.analysis.md +0 -15
- package/src/carousel/carousel.analysis.md +0 -17
- package/src/check-article-lang.analysis.md +0 -15
- package/src/directoryNodes/tree-of-content.analysis.md +0 -14
- package/src/directoryNodes/tree-of-content.stories.tsx +0 -22
- package/src/documents/result-list.analysis.md +0 -14
- package/src/documents/result-list.stories.tsx +0 -19
- package/src/favorites/bookmark-button.analysis.md +0 -17
- package/src/favorites/bookmark-button.stories.tsx +0 -19
- package/src/favorites/favorite-button.analysis.md +0 -18
- package/src/favorites/favorite-button.stories.tsx +0 -22
- package/src/icons/file-icon.analysis.md +0 -14
- package/src/icons/file-icon.stories.tsx +0 -19
- package/src/icons/flag-icon.analysis.md +0 -14
- package/src/icons/flag-icon.stories.tsx +0 -25
- package/src/icons/loading.analysis.md +0 -14
- package/src/icons/loading.stories.tsx +0 -21
- package/src/info/info-table.analysis.md +0 -15
- package/src/info/shared.analysis.md +0 -14
- package/src/info/stories/info-table.stories.tsx +0 -31
- package/src/info/stories/shared.stories.tsx +0 -24
- package/src/navbar/language-switcher/content-language-switch.analysis.md +0 -15
- package/src/navbar/language-switcher/shared.analysis.md +0 -14
- package/src/navbar/language-switcher/ui-language-switch.analysis.md +0 -15
- package/src/navbar/navbar.analysis.md +0 -14
- package/src/navbar/settings.analysis.md +0 -14
- package/src/navbar/sign-in-out-btns.analysis.md +0 -14
- package/src/navbar/stories/navbar.stories.tsx +0 -31
- package/src/navbar/stories/settings.stories.tsx +0 -15
- package/src/navbar/stories/sign-in-out-btns.stories.tsx +0 -15
- package/src/navbar/stories/user-menu.stories.tsx +0 -20
- package/src/navbar/user-menu.analysis.md +0 -14
- package/src/page-wrapper.analysis.md +0 -14
- package/src/render-article.analysis.md +0 -15
- package/src/renditions/file-download.analysis.md +0 -14
- package/src/renditions/file-download.stories.tsx +0 -19
- package/src/renditions/html.analysis.md +0 -17
- package/src/renditions/html.stories.tsx +0 -19
- package/src/renditions/image/container.analysis.md +0 -15
- package/src/renditions/image/container.stories.tsx +0 -19
- package/src/renditions/image/rendition.analysis.md +0 -14
- package/src/renditions/image/rendition.stories.tsx +0 -19
- package/src/restriction-menu/restriction-menu-container.analysis.md +0 -14
- package/src/restriction-menu/restriction-menu-item.analysis.md +0 -14
- package/src/restriction-menu/restriction-menu.analysis.md +0 -17
- package/src/results/analysis/cards.analysis.md +0 -14
- package/src/results/analysis/dialog-filter.analysis.md +0 -17
- package/src/results/analysis/empty.analysis.md +0 -14
- package/src/results/analysis/filter-navbar.analysis.md +0 -16
- package/src/results/analysis/pagination.analysis.md +0 -14
- package/src/results/analysis/table-with-images.analysis.md +0 -15
- package/src/results/analysis/table.analysis.md +0 -15
- package/src/results/filter-sidebar/index.analysis.md +0 -14
- package/src/results/generic/table-result-list.analysis.md +0 -15
- package/src/results/generic/table-result-list.stories.tsx +0 -21
- package/src/results/stories/cards.stories.tsx +0 -66
- package/src/results/stories/dialog-filter.stories.tsx +0 -20
- package/src/results/stories/empty.stories.tsx +0 -25
- package/src/results/stories/filter-navbar.stories.tsx +0 -19
- package/src/results/stories/filter-sidebar.stories.tsx +0 -20
- package/src/results/stories/pagination.stories.tsx +0 -24
- package/src/results/stories/table-with-images.stories.tsx +0 -19
- package/src/results/stories/table.stories.tsx +0 -78
- package/src/search-input.analysis.md +0 -15
- package/src/share-button.analysis.md +0 -19
- package/src/stories/autocomplete.stories.tsx +0 -20
- package/src/stories/breadcrumb.stories.tsx +0 -93
- package/src/stories/check-article-lang.stories.tsx +0 -22
- package/src/stories/render-article.stories.tsx +0 -19
- package/src/stories/search-input.stories.tsx +0 -21
- package/src/stories/share-button.stories.tsx +0 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c-rex/components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"files": [
|
|
5
5
|
"src"
|
|
6
6
|
],
|
|
@@ -179,31 +179,19 @@
|
|
|
179
179
|
}
|
|
180
180
|
},
|
|
181
181
|
"scripts": {
|
|
182
|
-
"storybook": "storybook dev -p 6006",
|
|
183
|
-
"build-storybook": "storybook build",
|
|
184
182
|
"lint": "eslint .",
|
|
185
183
|
"lint:fix": "eslint . --fix"
|
|
186
184
|
},
|
|
187
185
|
"devDependencies": {
|
|
188
186
|
"@c-rex/eslint-config": "*",
|
|
189
187
|
"@c-rex/typescript-config": "*",
|
|
190
|
-
"@chromatic-com/storybook": "^3.2.6",
|
|
191
|
-
"@storybook/addon-essentials": "^8.6.12",
|
|
192
|
-
"@storybook/addon-onboarding": "^8.6.12",
|
|
193
|
-
"@storybook/blocks": "^8.6.12",
|
|
194
|
-
"@storybook/nextjs": "^8.6.12",
|
|
195
|
-
"@storybook/react": "^8.6.12",
|
|
196
|
-
"@storybook/test": "^8.6.12",
|
|
197
188
|
"@turbo/gen": "^2.4.4",
|
|
198
189
|
"@types/node": "^22.13.10",
|
|
199
190
|
"@types/react": "19.0.10",
|
|
200
191
|
"@types/react-dom": "19.0.4",
|
|
201
192
|
"autoprefixer": "^10.4.21",
|
|
202
193
|
"eslint": "^9.23.0",
|
|
203
|
-
"eslint-plugin-storybook": "^0.12.0",
|
|
204
194
|
"postcss": "^8.5.3",
|
|
205
|
-
"storybook": "^8.6.12",
|
|
206
|
-
"style-loader": "^4.0.0",
|
|
207
195
|
"tailwindcss": "^3.4.17",
|
|
208
196
|
"typescript": "latest"
|
|
209
197
|
},
|
|
@@ -226,10 +214,5 @@
|
|
|
226
214
|
"react-icons": "^5.5.0",
|
|
227
215
|
"tailwindcss-animate": "^1.0.7",
|
|
228
216
|
"zustand": "^5.0.8"
|
|
229
|
-
},
|
|
230
|
-
"eslintConfig": {
|
|
231
|
-
"extends": [
|
|
232
|
-
"plugin:storybook/recommended"
|
|
233
|
-
]
|
|
234
217
|
}
|
|
235
218
|
}
|
|
@@ -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
|
@@ -8,6 +8,7 @@ import { X } from "lucide-react";
|
|
|
8
8
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
9
9
|
import { suggestionRequest } from "./generated/create-suggestions-request";
|
|
10
10
|
import { useQueryState } from "nuqs";
|
|
11
|
+
import { useSearchSettingsStore } from "./stores/search-settings-store";
|
|
11
12
|
|
|
12
13
|
export type AutoCompleteProps = {
|
|
13
14
|
initialValue?: string;
|
|
@@ -36,6 +37,7 @@ export const AutoComplete = ({
|
|
|
36
37
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
37
38
|
const searchParams = useSearchParams();
|
|
38
39
|
const router = useRouter();
|
|
40
|
+
|
|
39
41
|
const [open, setOpen] = useState(false);
|
|
40
42
|
const [query, setQuery] = useState(initialValue);
|
|
41
43
|
const [loading, setLoading] = useState(false);
|
|
@@ -43,19 +45,40 @@ export const AutoComplete = ({
|
|
|
43
45
|
|
|
44
46
|
const fetchSuggestions = useCallback(async (prefix: string): Promise<string[]> => {
|
|
45
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;
|
|
46
62
|
if (pkg != null) params.scopes = pkg as unknown as string[];
|
|
47
63
|
|
|
48
64
|
const results = await suggestionRequest({ endpoint, prefix, queryParams: params });
|
|
49
65
|
|
|
50
66
|
return results.data;
|
|
51
|
-
}, [endpoint, pkg, queryParams]);
|
|
67
|
+
}, [endpoint, pkg, queryParams, searchParams]);
|
|
52
68
|
|
|
53
69
|
const handleSelect = (value: string) => {
|
|
54
70
|
setQuery(value);
|
|
55
71
|
setOpen(false);
|
|
72
|
+
|
|
56
73
|
const nextParams = new URLSearchParams(keepParams ? searchParams.toString() : "");
|
|
57
74
|
nextParams.set("page", "1");
|
|
58
|
-
|
|
75
|
+
|
|
76
|
+
if (value.length > 0) {
|
|
77
|
+
nextParams.set("search", value);
|
|
78
|
+
} else {
|
|
79
|
+
nextParams.delete("search");
|
|
80
|
+
};
|
|
81
|
+
|
|
59
82
|
onSelectParams?.forEach(param => {
|
|
60
83
|
nextParams.set(param.key, param.value);
|
|
61
84
|
});
|
|
@@ -77,9 +100,7 @@ export const AutoComplete = ({
|
|
|
77
100
|
router.push(queryString ? `${onSelectPath}?${queryString}` : onSelectPath);
|
|
78
101
|
};
|
|
79
102
|
|
|
80
|
-
useEffect(() =>
|
|
81
|
-
setQuery(initialValue);
|
|
82
|
-
}, [initialValue]);
|
|
103
|
+
useEffect(() => setQuery(initialValue), [initialValue]);
|
|
83
104
|
|
|
84
105
|
useEffect(() => {
|
|
85
106
|
const handleClickOutside = (e: MouseEvent) => {
|
|
@@ -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
|
|
|
@@ -324,9 +322,10 @@ const DefaultRenderCarouselItem: FC<{ item: CommonItemsModel; showImages: boolea
|
|
|
324
322
|
const itemType = getType(item.class);
|
|
325
323
|
const language = getLanguage(item.languages)
|
|
326
324
|
const countryCode = language.split("-")[1] || "";
|
|
325
|
+
const link = linkPattern.replace("{shortId}", item.shortId!);
|
|
327
326
|
|
|
328
327
|
return (
|
|
329
|
-
<
|
|
328
|
+
<Link href={link} className="group p-2 flex flex-1">
|
|
330
329
|
<Card className="p-4 flex-1 justify-between relative">
|
|
331
330
|
<Badge className="absolute -top-2 -right-2">{t(itemType.toLowerCase())}</Badge>
|
|
332
331
|
|
|
@@ -338,15 +337,17 @@ const DefaultRenderCarouselItem: FC<{ item: CommonItemsModel; showImages: boolea
|
|
|
338
337
|
/>
|
|
339
338
|
)}
|
|
340
339
|
|
|
341
|
-
<span className="text-lg font-semibold">
|
|
340
|
+
<span className="group-hover:underline text-lg font-semibold flex-1">
|
|
341
|
+
{title}
|
|
342
|
+
</span>
|
|
342
343
|
|
|
343
344
|
<div className="flex justify-between w-full">
|
|
344
|
-
<span className="w-8 block
|
|
345
|
+
<span className="w-8 block">
|
|
345
346
|
<Flag countryCode={countryCode} />
|
|
346
347
|
</span>
|
|
347
348
|
<span className="text-gray-400">{date || item.revision}</span>
|
|
348
349
|
</div>
|
|
349
350
|
</Card>
|
|
350
|
-
</
|
|
351
|
+
</Link>
|
|
351
352
|
);
|
|
352
353
|
};
|
|
@@ -14,11 +14,15 @@ interface Props {
|
|
|
14
14
|
export const CheckArticleLangToast: FC<Props> = ({ availableVersions }) => {
|
|
15
15
|
const t = useTranslations();
|
|
16
16
|
const { setAvailableVersions } = useAppConfig()
|
|
17
|
+
const searchLanguage = useSearchSettingsStore((state) => state.language);
|
|
17
18
|
|
|
18
19
|
useEffect(() => {
|
|
19
20
|
setAvailableVersions(availableVersions)
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
}, [availableVersions, setAvailableVersions])
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!searchLanguage) return;
|
|
25
|
+
const activeArticle = availableVersions.find((item) => item.active)
|
|
22
26
|
|
|
23
27
|
if (activeArticle == undefined || activeArticle.lang == searchLanguage) return
|
|
24
28
|
|
|
@@ -26,7 +30,7 @@ export const CheckArticleLangToast: FC<Props> = ({ availableVersions }) => {
|
|
|
26
30
|
if (articleAvailable != undefined) {
|
|
27
31
|
articleAvailableInToast(articleAvailable.lang, articleAvailable.link)
|
|
28
32
|
}
|
|
29
|
-
}, [])
|
|
33
|
+
}, [availableVersions, searchLanguage])
|
|
30
34
|
|
|
31
35
|
const articleAvailableInToast = (lang: string, link: string) => {
|
|
32
36
|
toast(t('toast.read', { lang }), {
|
|
@@ -40,4 +44,4 @@ export const CheckArticleLangToast: FC<Props> = ({ availableVersions }) => {
|
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
return null
|
|
43
|
-
}
|
|
47
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { ComponentProps, FC
|
|
4
|
-
import { Button
|
|
3
|
+
import { ComponentProps, FC } from "react";
|
|
4
|
+
import { Button } 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";
|
|
8
8
|
import { useFavoritesStore } from "../stores/favorites-store";
|
|
9
9
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@c-rex/ui/dialog";
|
|
10
|
+
import { useTranslations } from "next-intl";
|
|
10
11
|
import {
|
|
11
12
|
Table,
|
|
12
13
|
TableBody,
|
|
@@ -15,20 +16,22 @@ import {
|
|
|
15
16
|
} from "@c-rex/ui/table"
|
|
16
17
|
import { FaRegBookmark } from "react-icons/fa6";
|
|
17
18
|
|
|
19
|
+
const EMPTY_TOPICS: { id: string; label: string; color: string }[] = [];
|
|
20
|
+
|
|
18
21
|
type BookmarkProps = {
|
|
19
22
|
shortId: string;
|
|
23
|
+
linkPattern?: string;
|
|
20
24
|
triggerVariant?: ComponentProps<typeof Button>["variant"];
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export const BookmarkButton: FC<BookmarkProps> = ({
|
|
24
28
|
shortId,
|
|
29
|
+
linkPattern = `/topics/{id}/pages`,
|
|
25
30
|
triggerVariant = "outline"
|
|
26
31
|
}) => {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
setMarkersList(useFavoritesStore.getState().documents[shortId]?.topics || []);
|
|
31
|
-
}, [shortId]);
|
|
32
|
+
const t = useTranslations("bookmarks");
|
|
33
|
+
const removeFavoriteTopic = useFavoritesStore((state) => state.unfavoriteTopic);
|
|
34
|
+
const markersList = useFavoritesStore((state) => state.documents[shortId]?.topics) ?? EMPTY_TOPICS;
|
|
32
35
|
return (
|
|
33
36
|
<Dialog>
|
|
34
37
|
<DialogTrigger asChild>
|
|
@@ -47,25 +50,37 @@ export const BookmarkButton: FC<BookmarkProps> = ({
|
|
|
47
50
|
</DialogTrigger>
|
|
48
51
|
<DialogContent>
|
|
49
52
|
<DialogHeader>
|
|
50
|
-
<DialogTitle>
|
|
53
|
+
<DialogTitle>{t("title")}</DialogTitle>
|
|
51
54
|
<DialogDescription>
|
|
52
|
-
|
|
55
|
+
{t("description")}
|
|
53
56
|
</DialogDescription>
|
|
54
57
|
</DialogHeader>
|
|
55
58
|
<Table>
|
|
56
59
|
<TableBody>
|
|
60
|
+
{markersList.length === 0 && (
|
|
61
|
+
<TableRow>
|
|
62
|
+
<TableCell colSpan={3} className="text-center">
|
|
63
|
+
{t("empty")}
|
|
64
|
+
</TableCell>
|
|
65
|
+
</TableRow>
|
|
66
|
+
)}
|
|
67
|
+
|
|
57
68
|
{markersList.map((item) => (
|
|
58
69
|
<TableRow key={item.id} className="min-h-12">
|
|
59
70
|
<TableCell>
|
|
60
71
|
<FaRegBookmark className={cn("w-5", `text-${item.color}`)} />
|
|
61
72
|
</TableCell>
|
|
62
73
|
<TableCell>
|
|
63
|
-
<Link href={
|
|
74
|
+
<Link href={linkPattern.replace("{id}", item.id)} className="hover:underline">
|
|
64
75
|
{item.label}
|
|
65
76
|
</Link>
|
|
66
77
|
</TableCell>
|
|
67
78
|
<TableCell>
|
|
68
|
-
<Button
|
|
79
|
+
<Button
|
|
80
|
+
variant="destructive"
|
|
81
|
+
size="icon"
|
|
82
|
+
onClick={() => removeFavoriteTopic(shortId, item.id)}
|
|
83
|
+
>
|
|
69
84
|
<Trash className="w-5 hover:text-red-600 cursor-pointer" />
|
|
70
85
|
</Button>
|
|
71
86
|
</TableCell>
|
|
@@ -6,6 +6,8 @@ import { FaStar, FaRegStar } from "react-icons/fa";
|
|
|
6
6
|
import { useFavoritesStore } from "../stores/favorites-store";
|
|
7
7
|
import { MARKER_COLORS, RESULT_TYPES } from "@c-rex/constants";
|
|
8
8
|
import { ResultTypes } from "@c-rex/types";
|
|
9
|
+
import { Loader2 } from "lucide-react";
|
|
10
|
+
import { toast } from "sonner";
|
|
9
11
|
|
|
10
12
|
export const FavoriteButton: FC<{ id: string, type: ResultTypes, label: string }> = ({ id, type, label }) => {
|
|
11
13
|
const addFavoriteTopic = useFavoritesStore((state) => state.favoriteTopic);
|
|
@@ -17,58 +19,102 @@ export const FavoriteButton: FC<{ id: string, type: ResultTypes, label: string }
|
|
|
17
19
|
const favoriteList = useFavoritesStore((state) => state.favorites);
|
|
18
20
|
const isFavorite = favoriteList.find((fav) => fav.id === id);
|
|
19
21
|
const [documentData, setDocumentData] = useState<{ id: string, label: string }>({ id, label });
|
|
22
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
20
23
|
|
|
21
24
|
useEffect(() => {
|
|
22
25
|
if (type === RESULT_TYPES.TOPIC) {
|
|
23
|
-
getTopicDocumentData(id)
|
|
26
|
+
void getTopicDocumentData(id).catch(() => {
|
|
27
|
+
// Lazy retry on user action.
|
|
28
|
+
});
|
|
24
29
|
}
|
|
25
|
-
}, []);
|
|
30
|
+
}, [id, type]);
|
|
26
31
|
|
|
27
|
-
const
|
|
32
|
+
const ensureDocumentData = async (topicId: string): Promise<{ id: string, label: string }> => {
|
|
33
|
+
if (type !== RESULT_TYPES.TOPIC) {
|
|
34
|
+
return { id, label };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (documentData.id !== topicId) {
|
|
38
|
+
return documentData;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return await getTopicDocumentData(topicId);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const addFavorite = async (targetId: string) => {
|
|
28
45
|
if (type === RESULT_TYPES.DOCUMENT) {
|
|
29
|
-
addFavoriteDocument(
|
|
46
|
+
addFavoriteDocument(targetId, label);
|
|
30
47
|
return;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
|
-
const
|
|
50
|
+
const docData = await ensureDocumentData(targetId);
|
|
51
|
+
const length = favoriteDocumentList[docData.id]?.topics.length || 0;
|
|
34
52
|
const color = MARKER_COLORS[length] || MARKER_COLORS[MARKER_COLORS.length - 1] as string;
|
|
35
53
|
|
|
36
|
-
addFavoriteTopic(
|
|
37
|
-
}
|
|
54
|
+
addFavoriteTopic(docData.id, targetId, label, color);
|
|
55
|
+
};
|
|
38
56
|
|
|
39
|
-
const removeFavorite = (
|
|
57
|
+
const removeFavorite = async (targetId: string) => {
|
|
40
58
|
if (type === RESULT_TYPES.DOCUMENT) {
|
|
41
|
-
removeFavoriteDocument(
|
|
59
|
+
removeFavoriteDocument(targetId);
|
|
42
60
|
return;
|
|
43
61
|
}
|
|
44
62
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const getTopicDocumentData = async (topicId: string): Promise<void> => {
|
|
63
|
+
const docData = await ensureDocumentData(targetId);
|
|
64
|
+
removeFavoriteTopic(docData.id, targetId);
|
|
65
|
+
};
|
|
49
66
|
|
|
67
|
+
const getTopicDocumentData = async (topicId: string): Promise<{ id: string, label: string }> => {
|
|
50
68
|
const response = await fetch(`/api/information-units/document-by-topic-id?shortId=${topicId}`, {
|
|
51
69
|
method: "GET"
|
|
52
70
|
});
|
|
53
71
|
|
|
54
|
-
if (!response.ok)
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error("Failed to fetch document by topic id");
|
|
74
|
+
}
|
|
55
75
|
|
|
56
76
|
const { documentId, label } = await response.json();
|
|
57
|
-
|
|
58
|
-
setDocumentData(
|
|
59
|
-
|
|
77
|
+
const data = { id: documentId, label };
|
|
78
|
+
setDocumentData(data);
|
|
79
|
+
return data;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handleToggle = async () => {
|
|
83
|
+
if (isLoading) return;
|
|
84
|
+
setIsLoading(true);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (isFavorite) {
|
|
88
|
+
await removeFavorite(id);
|
|
89
|
+
} else {
|
|
90
|
+
await addFavorite(id);
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
toast.error("Não foi possível atualizar os favoritos.");
|
|
94
|
+
} finally {
|
|
95
|
+
setIsLoading(false);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
60
98
|
|
|
61
99
|
if (isFavorite) {
|
|
62
100
|
return (
|
|
63
|
-
<Button variant="ghost" size="icon" onClick={
|
|
64
|
-
|
|
101
|
+
<Button variant="ghost" size="icon" onClick={handleToggle} disabled={isLoading}>
|
|
102
|
+
{isLoading ? (
|
|
103
|
+
<Loader2 className="!h-5 !w-5 animate-spin" />
|
|
104
|
+
) : (
|
|
105
|
+
<FaStar className="!h-5 !w-5 color-primary" />
|
|
106
|
+
)}
|
|
65
107
|
</Button>
|
|
66
108
|
);
|
|
67
109
|
}
|
|
68
110
|
|
|
69
111
|
return (
|
|
70
|
-
<Button variant="ghost" size="icon" onClick={
|
|
71
|
-
|
|
112
|
+
<Button variant="ghost" size="icon" onClick={handleToggle} disabled={isLoading}>
|
|
113
|
+
{isLoading ? (
|
|
114
|
+
<Loader2 className="!h-5 !w-5 animate-spin" />
|
|
115
|
+
) : (
|
|
116
|
+
<FaRegStar className="!h-5 !w-5" />
|
|
117
|
+
)}
|
|
72
118
|
</Button>
|
|
73
119
|
)
|
|
74
120
|
}
|