@c-rex/components 0.1.36 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-rex/components",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "files": [
5
5
  "src"
6
6
  ],
@@ -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
- if (filteredLinks.length == 0 || filteredLinks[0] == undefined || filteredLinks[0].href == undefined) return empty;
14
+ const articleRender = (html: string) => {
25
15
 
26
- const url = filteredLinks[0].href;
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
- const $ = cheerio.load(html)
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
- const metaTags = $("meta").map((_, el) => {
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
- const articleHtml = $("main").html() || ""
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
- {metaTags.map((tag) => (
47
- <meta key={`${tag.name}-${tag.content}`} name={tag.name} content={tag.content} />
48
- ))}
49
- <div className="pr-4 relative">
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
  }
@@ -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
- nextParams.set("search", value);
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
- // Types
27
-
28
- type ResponsiveSetting = {
29
- maxWidth: number;
30
- slidesToShow: number;
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
- responsive = [{ maxWidth: 99999, slidesToShow: 1 }],
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
- // Responsive
93
- const [slidesToShow, setSlidesToShow] = useState(responsive[0]?.slidesToShow || 1);
94
- const updateSlidesToShow = useCallback(() => {
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
- updateSlidesToShow();
108
- window.addEventListener("resize", updateSlidesToShow);
109
- return () => window.removeEventListener("resize", updateSlidesToShow);
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="size-8" rounded="full" onClick={next} variant="ghost" disabled={disabled}>
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="size-8" rounded="full" onClick={prev} variant="ghost" disabled={disabled}>
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<{ item: CommonItemsModel; showImages: boolean }> = ({ item, showImages }) => {
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
- <span className="text-lg font-semibold">{title}</span>
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">
@@ -30,7 +30,7 @@ export const NavBar: FC<NavBarProps> = async ({
30
30
  showInput,
31
31
  autocompleteType,
32
32
  onSelectPath,
33
- showMenu = false,
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>
@@ -1,11 +1,14 @@
1
1
  import { FC, JSX } from "react";
2
2
  import * as cheerio from "cheerio"
3
- import { FragmentsGetAll } from "../generated/server-components";
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
- return (
25
- <FragmentsGetAll
26
- queryParams={{
27
- Fields: ["titles", "renditions"],
28
- Embed: ["renditions"],
29
- PageSize: 1,
30
- Links: true,
31
- Restrict: [
32
- `informationUnits=${shortId}`,
33
- ...htmlFormats.map(format => `renditions.format=${format}`)
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
- render={async (data, error) => {
38
- if (error) {
39
- return <div>Error loading content</div>;
40
- }
40
+ renditions = result.items?.[0]?.renditions;
41
+ }
41
42
 
42
- const renditions = data?.items?.[0]?.renditions;
43
+ if (renditions == null || renditions.length == 0) return empty;
44
+ renditions = renditions.filter(rendition => htmlFormats.includes(rendition.format!));
43
45
 
44
- if (renditions == null || renditions.length == 0) return empty;
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
- const filteredLinks = renditions[0].links.filter((item) => item.rel == "view");
48
+ const filteredLinks = renditions[0].links.filter((item) => item.rel == "view");
48
49
 
49
- if (filteredLinks.length == 0 || filteredLinks[0] == undefined || filteredLinks[0].href == undefined) return empty;
50
+ if (filteredLinks.length == 0 || filteredLinks[0] == undefined || filteredLinks[0].href == undefined) return empty;
50
51
 
51
- const url = filteredLinks[0].href;
52
- const html = await fetch(url).then(res => res.text());
52
+ const url = filteredLinks[0].href;
53
+ try {
53
54
 
54
- return render ? render(html) : defaultRender(html);
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
- className="text-left text-wrap !h-auto w-full !justify-start cursor-pointer"
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="All"
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
- <DropdownHoverItem
135
- label={
136
- <Button variant="outline" rounded="full">
137
- More <ChevronDown className="size-4" />
138
- </Button>
139
- }
140
- >
141
- {hiddenItems.map((item) => (
142
- <RestrictionDropdownItem
143
- key={item.shortId}
144
- shortId={item.shortId!}
145
- restrictField={restrictField as string}
146
- label={getLabelByLang(item.labels, lang)}
147
- selected={restrictionValues.includes(item.shortId!)}
148
- />
149
- ))}
150
- </DropdownHoverItem>
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>