@c-rex/components 0.1.15 → 0.1.17

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.15",
3
+ "version": "0.1.17",
4
4
  "files": [
5
5
  "src"
6
6
  ],
@@ -25,6 +25,10 @@
25
25
  "types": "./src/breadcrumb.tsx",
26
26
  "import": "./src/breadcrumb.tsx"
27
27
  },
28
+ "./settings-menu": {
29
+ "types": "./src/navbar/settings.tsx",
30
+ "import": "./src/navbar/settings.tsx"
31
+ },
28
32
  "./flag": {
29
33
  "types": "./src/flag.tsx",
30
34
  "import": "./src/flag.tsx"
@@ -101,6 +105,10 @@
101
105
  "types": "./src/loading.tsx",
102
106
  "import": "./src/loading.tsx"
103
107
  },
108
+ "./favorite-button": {
109
+ "types": "./src/favorite-button.tsx",
110
+ "import": "./src/favorite-button.tsx"
111
+ },
104
112
  "./language-store": {
105
113
  "types": "./src/stores/language-store.ts",
106
114
  "import": "./src/stores/language-store.ts"
@@ -108,6 +116,10 @@
108
116
  "./highlight-store": {
109
117
  "types": "./src/stores/highlight-store.ts",
110
118
  "import": "./src/stores/highlight-store.ts"
119
+ },
120
+ "./favorites-store": {
121
+ "types": "./src/stores/favorites-store.ts",
122
+ "import": "./src/stores/favorites-store.ts"
111
123
  }
112
124
  },
113
125
  "scripts": {
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import { useEffect, useRef, useState } from "react";
2
4
  import { Input } from "@c-rex/ui/input";
3
5
  import { call, generateQueryParams } from "@c-rex/utils";
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import { FC, ReactNode } from "react";
4
+ import { Button } from "@c-rex/ui/button";
5
+ import { Trash } from "lucide-react";
6
+ import { cn } from "@c-rex/utils";
7
+ import Link from "next/link";
8
+ import { Favorite } from "@c-rex/types";
9
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@c-rex/ui/dialog";
10
+ import {
11
+ Table,
12
+ TableBody,
13
+ TableCell,
14
+ TableRow,
15
+ } from "@c-rex/ui/table"
16
+ import { FaRegBookmark } from "react-icons/fa6";
17
+
18
+ export const BookmarkButton: FC<{ markersList: Favorite[], trigger?: ReactNode }> = ({ markersList, trigger }) => {
19
+
20
+ return (
21
+ <Dialog>
22
+ <DialogTrigger asChild>
23
+
24
+ {trigger ? trigger : (
25
+
26
+ <Button variant="outline" size="icon" className="relative">
27
+ <FaRegBookmark className="text-primary" />
28
+ <span
29
+ className="absolute -top-[10px] -right-[10px] min-w-5 min-h-5 bg-primary text-white rounded-full"
30
+ >
31
+ {markersList.length}
32
+ </span>
33
+ </Button>
34
+ )}
35
+ </DialogTrigger>
36
+ <DialogContent>
37
+ <DialogHeader>
38
+ <DialogTitle>Bookmarks</DialogTitle>
39
+ <DialogDescription>
40
+ Manage your bookmarks here
41
+ </DialogDescription>
42
+ </DialogHeader>
43
+ <Table>
44
+ <TableBody>
45
+ {markersList.map((item) => (
46
+ <TableRow key={item.id} className="min-h-12">
47
+ <TableCell>
48
+ <FaRegBookmark className={cn("w-5", `text-${item.color}`)} />
49
+ </TableCell>
50
+ <TableCell>
51
+ <Link href={`./${item.id}`}>
52
+ {item.label}
53
+ </Link>
54
+ </TableCell>
55
+ <TableCell>
56
+ <Button variant="destructive" size="icon">
57
+ <Trash className="w-5 hover:text-red-600 cursor-pointer" />
58
+ </Button>
59
+ </TableCell>
60
+ </TableRow>
61
+ ))}
62
+ </TableBody>
63
+ </Table>
64
+ </DialogContent>
65
+ </Dialog>
66
+ )
67
+ }
@@ -1,4 +1,4 @@
1
- import React, { FC, useState } from "react"
1
+ import React, { FC, useEffect, useState } from "react"
2
2
  import { Button } from "@c-rex/ui/button"
3
3
  import {
4
4
  Dialog,
@@ -35,17 +35,18 @@ export const DialogFilter: FC<DialogFilterProps> = ({ trigger }) => {
35
35
  const t = useTranslations("filter");
36
36
  const { setLoading } = useSearchContext();
37
37
 
38
- const savedLike = useSearchSettingsStore.getState().like;
39
- const savedOperator = useSearchSettingsStore.getState().operator;
40
- const savedWildcard = useSearchSettingsStore.getState().wildcard;
41
- const savedLanguages = useLanguageStore.getState().contentLang;
42
- const availableLanguagesAndCountries = useLanguageStore.getState().availableLanguages;
38
+ const savedLike = useSearchSettingsStore((state) => state.like)
39
+ const savedOperator = useSearchSettingsStore((state) => state.operator)
40
+ const savedWildcard = useSearchSettingsStore((state) => state.wildcard)
41
+ const savedLanguages = useLanguageStore(state => state.contentLang);
42
+ const availableLanguagesAndCountries = useLanguageStore(state => state.availableLanguages);
43
43
 
44
44
  const [like, setLike] = useState<boolean>(savedLike);
45
45
  const [operator, setOperator] = useState<string>(savedOperator);
46
46
  const [wildcard, setWildcard] = useState<string>(savedWildcard);
47
- const [languages, setLanguages] = useState<Languages[]>(
48
- availableLanguagesAndCountries?.map((item: LanguageAndCountries) => {
47
+
48
+ const generateLanguagesList = (): Languages[] => {
49
+ return availableLanguagesAndCountries?.map((item: LanguageAndCountries) => {
49
50
  const checked = savedLanguages.includes(item.value)
50
51
 
51
52
  return {
@@ -54,8 +55,9 @@ export const DialogFilter: FC<DialogFilterProps> = ({ trigger }) => {
54
55
  checked,
55
56
  }
56
57
  })
57
- );
58
+ }
58
59
 
60
+ const [languages, setLanguages] = useState<Languages[]>(generateLanguagesList());
59
61
  const [params, setParams] = useQueryStates({
60
62
  search: parseAsString,
61
63
  language: parseAsString,
@@ -100,6 +102,10 @@ export const DialogFilter: FC<DialogFilterProps> = ({ trigger }) => {
100
102
  });
101
103
  }
102
104
 
105
+ useEffect(() => {
106
+ setLanguages(generateLanguagesList())
107
+ }, [availableLanguagesAndCountries])
108
+
103
109
  return (
104
110
  <Dialog>
105
111
  <DialogTrigger asChild>
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import { FC, useEffect, useState } from "react";
4
+ import { Button } from "@c-rex/ui/button";
5
+ import { FaStar, FaRegStar } from "react-icons/fa";
6
+ import { useFavoritesStore } from "./stores/favorites-store";
7
+ import { MARKER_COLORS, RESULT_TYPES } from "@c-rex/constants";
8
+ import { ResultTypes } from "@c-rex/types";
9
+
10
+ export const FavoriteButton: FC<{ id: string, type: ResultTypes, label: string }> = ({ id, type, label }) => {
11
+ const addFavoriteTopic = useFavoritesStore((state) => state.favoriteTopic);
12
+ const addFavoriteDocument = useFavoritesStore((state) => state.favoriteDocument);
13
+ const removeFavoriteTopic = useFavoritesStore((state) => state.unfavoriteTopic);
14
+ const removeFavoriteDocument = useFavoritesStore((state) => state.unfavoriteDocument);
15
+
16
+ const favoriteDocumentList = useFavoritesStore((state) => state.documents);
17
+ const favoriteList = useFavoritesStore((state) => state.favorites);
18
+ const isFavorite = favoriteList.find((fav) => fav.id === id);
19
+ const [documentData, setDocumentData] = useState<{ id: string, label: string }>({ id, label });
20
+
21
+ useEffect(() => {
22
+ if (type === RESULT_TYPES.TOPIC) {
23
+ getTopicDocumentData(id);
24
+ }
25
+ }, []);
26
+
27
+ const addFavorite = async (id: string) => {
28
+ if (type === RESULT_TYPES.DOCUMENT) {
29
+ addFavoriteDocument(id, label);
30
+ return;
31
+ }
32
+
33
+ const length = favoriteDocumentList[documentData.id]?.topics.length || 0;
34
+ const color = MARKER_COLORS[length] || MARKER_COLORS[MARKER_COLORS.length - 1] as string;
35
+
36
+ addFavoriteTopic(documentData.id, id, label, color);
37
+ }
38
+
39
+ const removeFavorite = (id: string) => {
40
+ if (type === RESULT_TYPES.DOCUMENT) {
41
+ removeFavoriteDocument(id);
42
+ return;
43
+ }
44
+
45
+ removeFavoriteTopic(documentData.id, id);
46
+ }
47
+
48
+ const getTopicDocumentData = async (topicId: string): Promise<void> => {
49
+
50
+ const response = await fetch(`/api/information-units/document-by-topic-id?shortId=${topicId}`, {
51
+ method: "GET"
52
+ });
53
+
54
+ if (!response.ok) throw new Error("Failed to fetch document by topic id")
55
+
56
+ const { documentId, label } = await response.json();
57
+
58
+ setDocumentData({ id: documentId, label });
59
+ }
60
+
61
+ if (isFavorite) {
62
+ return (
63
+ <Button variant="ghost" size="icon" onClick={() => removeFavorite(id)}>
64
+ <FaStar className="!h-5 !w-5 color-primary" />
65
+ </Button>
66
+ );
67
+ }
68
+
69
+ return (
70
+ <Button variant="ghost" size="icon" onClick={() => addFavorite(id)}>
71
+ <FaRegStar className="!h-5 !w-5" />
72
+ </Button>
73
+ )
74
+ }
@@ -0,0 +1,9 @@
1
+ import { PiFilePdf } from "react-icons/pi";
2
+
3
+ export const FileIcon = ({ extension }: { extension: string }) => {
4
+ const IconsToFileExtension: Record<string, React.ReactNode> = {
5
+ "application/pdf": <PiFilePdf className="!h-5 !w-5 text-primary" />,
6
+ };
7
+
8
+ return IconsToFileExtension[extension] || null;
9
+ };
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import React, { FC } from "react";
2
4
  import { Card, CardContent, CardHeader, CardTitle } from "@c-rex/ui/card";
3
5
  import { useTranslations } from "next-intl";
@@ -1,5 +1,5 @@
1
1
  import React, { FC } from "react";
2
- import { articleInfoItemType, DocumentsType } from "@c-rex/types";
2
+ import { articleInfoItemType, DocumentsType, Favorite } from "@c-rex/types";
3
3
  import { Card, CardContent, CardHeader, CardTitle } from "@c-rex/ui/card";
4
4
  import {
5
5
  Table,
@@ -16,22 +16,20 @@ import {
16
16
  DropdownMenuTrigger,
17
17
  } from "@c-rex/ui/dropdown-menu";
18
18
  import { CloudDownload, Eye } from "lucide-react";
19
-
20
- import { FaFilePdf } from "react-icons/fa6";
21
19
  import { AvailableVersionsInterface } from "@c-rex/interfaces";
22
20
  import { Flag } from "../flag";
21
+ import { Button } from "@c-rex/ui/button";
23
22
  import { useLanguageStore } from "../stores/language-store";
23
+ import { FileIcon } from "../file-icon";
24
24
 
25
25
  type Props = {
26
26
  title: string;
27
27
  files?: DocumentsType
28
28
  items: articleInfoItemType[]
29
29
  availableVersions?: AvailableVersionsInterface[]
30
+ markersList?: Favorite[]
30
31
  }
31
32
 
32
- const IconsToFileExtension: Record<string, React.ReactNode> = {
33
- "application/pdf": <FaFilePdf className="h-6 w-6" />,
34
- };
35
33
 
36
34
  export const InfoTable: FC<Props> = ({ title, items, files, availableVersions }) => {
37
35
  const t = useTranslations();
@@ -41,7 +39,9 @@ export const InfoTable: FC<Props> = ({ title, items, files, availableVersions })
41
39
  return (
42
40
  <Card className="p-0 !pt-4">
43
41
  <CardHeader>
44
- <CardTitle className="text-lg">{title}</CardTitle>
42
+ <CardTitle className="text-lg flex justify-between">
43
+ {title}
44
+ </CardTitle>
45
45
  </CardHeader>
46
46
  <CardContent className="space-y-3 !p-0">
47
47
  <Table>
@@ -63,6 +63,25 @@ export const InfoTable: FC<Props> = ({ title, items, files, availableVersions })
63
63
  </TableRow>
64
64
  ))}
65
65
 
66
+ {availableVersions && (
67
+ <TableRow className="min-h-12">
68
+ <TableCell className="font-medium w-28 pl-4">
69
+ <h4 className="text-sm font-medium">{t("availableIn")}</h4>
70
+ </TableCell>
71
+ <TableCell className="text-xs text-muted-foreground flex items-center gap-2 min-h-12">
72
+ {availableVersions.map((item) => {
73
+ return (
74
+ <span className="w-8 block border" key={item.shortId}>
75
+ <a href={item.link} title={item.lang}>
76
+ <Flag countryCode={item.country} />
77
+ </a>
78
+ </span>
79
+ )
80
+ })}
81
+ </TableCell>
82
+ </TableRow>
83
+ )}
84
+
66
85
  {files && (
67
86
  <TableRow className="min-h-12">
68
87
  <TableCell className="font-medium w-28 pl-4">
@@ -75,18 +94,20 @@ export const InfoTable: FC<Props> = ({ title, items, files, availableVersions })
75
94
 
76
95
  return (
77
96
  <DropdownMenu key={index}>
78
- <DropdownMenuTrigger className="mx-2">
79
- {IconsToFileExtension[item]}
97
+ <DropdownMenuTrigger className="mr-2" asChild >
98
+ <Button variant="outline" size="icon" >
99
+ <FileIcon extension={item} />
100
+ </Button>
80
101
  </DropdownMenuTrigger>
81
102
  <DropdownMenuContent>
82
103
  <DropdownMenuItem>
83
104
  <a href={files[item].view} target="_blank" rel="noreferrer" className="flex items-center">
84
- <Eye className="mr-2 " /> Open
105
+ <Eye className="mr-2" /> Open
85
106
  </a>
86
107
  </DropdownMenuItem>
87
108
  <DropdownMenuItem>
88
109
  <a href={files[item].download} target="_blank" rel="noreferrer" className="flex items-center">
89
- <CloudDownload className="mr-2 " /> Download
110
+ <CloudDownload className="mr-2" /> Download
90
111
  </a>
91
112
  </DropdownMenuItem>
92
113
 
@@ -98,29 +119,9 @@ export const InfoTable: FC<Props> = ({ title, items, files, availableVersions })
98
119
  </TableCell>
99
120
  </TableRow>
100
121
  )}
101
-
102
- {availableVersions && (
103
- <TableRow className="min-h-12">
104
- <TableCell className="font-medium w-28 pl-4">
105
- <h4 className="text-sm font-medium">{t("availableIn")}</h4>
106
- </TableCell>
107
- <TableCell className="text-xs text-muted-foreground flex items-center gap-2 min-h-12">
108
- {availableVersions.map((item) => {
109
- return (
110
- <span className="w-8 block border" key={item.shortId}>
111
- <a href={item.link} title={item.lang}>
112
- <Flag countryCode={item.country} />
113
- </a>
114
- </span>
115
- )
116
- })}
117
- </TableCell>
118
- </TableRow>
119
- )}
120
-
121
122
  </TableBody>
122
123
  </Table>
123
124
  </CardContent>
124
- </Card>
125
+ </Card >
125
126
  );
126
127
  }
@@ -2,10 +2,10 @@ import { FC } from "react";
2
2
  import Link from "next/link";
3
3
  import { SignInBtn } from "./sign-in-out-btns";
4
4
  import { getServerSession } from "next-auth";
5
- import { getClientConfigs, getServerConfigs } from "@c-rex/utils/next-cookies";
6
5
  import { SettingsMenu } from "./settings";
7
6
  import { UserMenu } from "./user-menu";
8
7
  import { SearchInput } from "./search-input";
8
+ import { CrexSDK } from "@c-rex/core/sdk";
9
9
 
10
10
  interface NavBarProps {
11
11
  title: string;
@@ -14,9 +14,9 @@ interface NavBarProps {
14
14
  }
15
15
 
16
16
  export const NavBar: FC<NavBarProps> = async ({ title, showInput, showPkgFilter }) => {
17
- const clientConfigs = await getClientConfigs();
18
- const serverConfigs = await getServerConfigs();
19
-
17
+ const sdk = new CrexSDK();
18
+ const clientConfigs = sdk.getClientConfig();
19
+ const serverConfigs = sdk.getServerConfig();
20
20
 
21
21
  let session: any;
22
22
  if (clientConfigs.OIDC.userEnabled) {
@@ -23,7 +23,7 @@ export const SearchInput: FC<Props> = ({ showInput, showPkgFilter, placedOn = "N
23
23
  placedOn === "NAVBAR" ? "hidden lg:flex" : "sm:flex-none sm:w-60 md:w-72 lg:w-full lg:hidden flex",
24
24
  "flex-1 items-center px-3 border rounded-full h-8 c-rex-search-bar"
25
25
  )}>
26
- <Search className="h-4 w-4 shrink-0 opacity-50" />
26
+ <Search className="shrink-0 opacity-50" />
27
27
 
28
28
  <AutoComplete
29
29
  initialValue=""
@@ -61,7 +61,7 @@ export const RenderArticle = ({ htmlContent, contentLang }: Props) => {
61
61
  }, [highlightedContent, registerContainer]);
62
62
 
63
63
  return (
64
- <main ref={containerRef} lang={contentLang} className="pb-4">
64
+ <main ref={containerRef} lang={contentLang} className="pb-4 pr-4 min-h-[70vh]">
65
65
  {highlightedContent}
66
66
  </main>
67
67
  );
@@ -1,10 +1,12 @@
1
- import React, { FC } from "react";
1
+ import React, { FC, ReactNode } from "react";
2
2
  import { DefaultPageInfo, informationUnitsResponseItem, TopicsResponseItem, } from "@c-rex/interfaces";
3
3
  import { Empty } from "./empty";
4
4
  import BlogView from './result-view/blog';
5
5
  import TableView from './result-view/table';
6
6
  import { Pagination } from "./pagination";
7
7
  import { useAppConfig } from "@c-rex/contexts/config-provider";
8
+ import { ResultViewStyles } from "@c-rex/types";
9
+ import TableWithImageView from "./result-view/table-with-images";
8
10
 
9
11
  interface ResultListProps {
10
12
  items: informationUnitsResponseItem[] | TopicsResponseItem[];
@@ -14,32 +16,28 @@ interface ResultListProps {
14
16
  export const ResultList: FC<ResultListProps> = ({ items, pagination }: ResultListProps) => {
15
17
  const { configs } = useAppConfig()
16
18
 
17
- const isTableView = configs.results.resultViewStyle === "table";
19
+ const listComponent: Record<ResultViewStyles, ReactNode> = {
20
+ "cards": <BlogView items={items as TopicsResponseItem[]} />,
21
+ "table": <TableView items={items as informationUnitsResponseItem[]} />,
22
+ "table-with-images": <TableWithImageView items={items as informationUnitsResponseItem[]} />,
23
+ }
24
+
25
+ if (!(configs.results.resultViewStyle in listComponent)) {
26
+ throw new Error(`Unsupported result view style: ${configs.results.resultViewStyle}`);
27
+ }
28
+
29
+ if (items.length === 0) {
30
+ return <Empty />;
31
+ }
18
32
 
19
33
  return (
20
34
  <>
21
- {
22
- items.length === 0 ? (
23
- <Empty />
24
- ) : (
25
- <>
26
- {isTableView ? (
27
- <TableView
28
- items={items as informationUnitsResponseItem[]}
29
- />
30
- ) : (
31
- <BlogView
32
- items={items as TopicsResponseItem[]}
33
- />
34
- )}
35
+ {listComponent[configs.results.resultViewStyle as ResultViewStyles]}
35
36
 
36
- <Pagination
37
- totalPages={pagination.pageCount}
38
- currentPage={pagination.pageNumber}
39
- />
40
- </>
41
- )
42
- }
37
+ <Pagination
38
+ totalPages={pagination.pageCount}
39
+ currentPage={pagination.pageNumber}
40
+ />
43
41
  </>
44
42
  );
45
43
  };
@@ -50,6 +50,7 @@ const BlogView: FC<BlogViewProps> = ({ items }) => {
50
50
  style={{
51
51
  width: "100%", height: index == 0 ? "auto" : "190px"
52
52
  }}
53
+ loading={index == 0 ? "eager" : "lazy"}
53
54
  onLoad={() => setLoading(false)}
54
55
  onError={() => setLoading(false)}
55
56
  />
@@ -0,0 +1,199 @@
1
+ import { FC, useEffect, useState } from "react";
2
+ import { informationUnitsResponseItem } from "@c-rex/interfaces";
3
+ import { CloudDownload, Eye, FileStack, ImageOff } from "lucide-react";
4
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@c-rex/ui/dropdown-menu";
5
+ import { cn } from "@c-rex/utils";
6
+ import { useQueryState } from "nuqs";
7
+ import { Skeleton } from "@c-rex/ui/skeleton";
8
+ import { FavoriteButton } from "../favorite-button";
9
+ import { Button } from "@c-rex/ui/button";
10
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@c-rex/ui/tooltip";
11
+ import { Favorite, ResultTypes } from "@c-rex/types";
12
+ import { FileIcon } from "../file-icon";
13
+ import { useFavoritesStore } from "../stores/favorites-store";
14
+ import { BookmarkButton } from "../bookmark-button";
15
+ import { FaRegBookmark } from "react-icons/fa6";
16
+
17
+ interface TableWithImageViewProps {
18
+ items: informationUnitsResponseItem[];
19
+ }
20
+
21
+ const Image: FC<{ id: string }> = ({ id }) => {
22
+ const [imageUrl, setImageUrl] = useState("");
23
+ const [loading, setLoading] = useState(true);
24
+ const [error, setError] = useState(false);
25
+
26
+ useEffect(() => {
27
+ let ignore = false;
28
+
29
+ async function fetchImage() {
30
+ try {
31
+ const res = await fetch(`https://picsum.photos/v2/list?limit=1`);
32
+
33
+ {/*
34
+ TODO: talvez vamos receber mais de uma imagem, mas por enquanto pegamos apenas a primeira na lista
35
+ */}
36
+
37
+
38
+ const data = await res.json();
39
+ if (!ignore) {
40
+ setImageUrl(data[0].download_url);
41
+ setLoading(false);
42
+ }
43
+ } catch (err) {
44
+ setError(true);
45
+ console.error("Error loading image", err);
46
+ }
47
+ }
48
+
49
+ fetchImage();
50
+ return () => {
51
+ ignore = true
52
+ };
53
+ }, [id]);
54
+
55
+ if (error) {
56
+ return <ImageOff className="h-16 w-16 text-muted" />;
57
+ }
58
+
59
+ return (
60
+ <div className="h-16 w-16 flex items-center">
61
+ {loading ? (
62
+ <Skeleton className="h-16 w-16" />
63
+ ) : (
64
+ <img
65
+ src={imageUrl}
66
+ loading="lazy"
67
+ onLoad={() => setLoading(false)}
68
+ />
69
+ )}
70
+ </div>
71
+ );
72
+ }
73
+
74
+ const TableWithImageView: FC<TableWithImageViewProps> = ({ items }) => {
75
+ const [query] = useQueryState("search");
76
+
77
+ const favoritesDocuments = useFavoritesStore((state) => state.documents);
78
+
79
+ return (
80
+ <div className="mb-6">
81
+ {items.map((item, index) => {
82
+ const isFavoriteDocument = Object.keys(favoritesDocuments).some((docId) => docId === item.shortId);
83
+ let topicList: Favorite[] = []
84
+
85
+ if (isFavoriteDocument && favoritesDocuments[item.shortId]) {
86
+ topicList = favoritesDocuments[item.shortId]?.topics || []
87
+ }
88
+ return (
89
+ <div
90
+ key={index}
91
+ className={cn(
92
+ "min-h-12 flex flex-wrap items-center border px-4 py-2 gap-2 rounded mb-2",
93
+ `c-rex-result-item c-rex-result-${item.type.toLowerCase()}`,
94
+ item.disabled && "c-rex-result-item-disabled",
95
+ )}
96
+ >
97
+
98
+ <div className="h-16 w-16 flex items-center">
99
+ <Image id={item.shortId} />
100
+ </div>
101
+
102
+ <div className="flex-1 p-2 flex flex-col">
103
+ <span className=" text-sm text-muted-foreground">
104
+ {item.language.split("-")[0]?.toUpperCase()}
105
+ </span>
106
+
107
+ <span className="text-lg font-medium">
108
+ {item.disabled ? (
109
+ item.title
110
+ ) : (
111
+ <a className="hover:underline" href={`${item.link}?q=${query}`}>{item.title}</a>
112
+ )}
113
+ </span>
114
+
115
+ <span className="text-sm">
116
+ {item.type}
117
+ </span>
118
+ <span className="text-sm">
119
+ short description here {/*
120
+ TODO: get abstract if has the value key on it. Same behavior on class props
121
+
122
+ abstract and description are not being returned by the API yet
123
+ */}
124
+ </span>
125
+ </div>
126
+
127
+ <div className="flex flex-col p-2 ml-auto justify-center">
128
+ <span className="text-end text-sm text-muted-foreground mb-2">
129
+ {item.revision}
130
+ </span>
131
+ <div className="flex gap-2">
132
+ {Object.keys(item.files).map((fileKey, index) => {
133
+ if (!item.files[fileKey]) return null
134
+
135
+ return (
136
+ <DropdownMenu key={index}>
137
+ <DropdownMenuTrigger>
138
+ <Button variant="ghost" size="icon">
139
+ <FileIcon extension={fileKey} />
140
+ </Button>
141
+ </DropdownMenuTrigger>
142
+ <DropdownMenuContent>
143
+ <DropdownMenuItem>
144
+ <a href={item.files[fileKey].view} target="_blank" rel="noreferrer" className="flex items-center">
145
+ <Eye className="mr-2" /> Open {/*TODO: use i18n functions*/}
146
+ </a>
147
+ </DropdownMenuItem>
148
+ <DropdownMenuItem>
149
+ <a href={item.files[fileKey].download} target="_blank" rel="noreferrer" className="flex items-center">
150
+ <CloudDownload className="mr-2" /> Download {/*TODO: use i18n functions*/}
151
+ </a>
152
+ </DropdownMenuItem>
153
+ </DropdownMenuContent>
154
+ </DropdownMenu>
155
+ )
156
+ })}
157
+
158
+ {(topicList && topicList.length > 0) && (
159
+ <BookmarkButton
160
+ markersList={topicList.filter(item => item.label.length > 0)}
161
+ trigger={
162
+ <Button variant="ghost" size="icon" className="relative">
163
+ <FaRegBookmark className="!w-5 color-primary" />
164
+ <span
165
+ className="absolute -top-[10px] -right-[10px] min-w-5 min-h-5 bg-primary text-white rounded-full"
166
+ >
167
+ {topicList.length}
168
+ </span>
169
+ </Button>
170
+ }
171
+ />
172
+ )}
173
+
174
+ <FavoriteButton id={item.shortId} type={item.type as ResultTypes} label={item.title} />
175
+
176
+ {item.multipleVersions.length > 1 && (
177
+ <Tooltip>
178
+ <TooltipTrigger asChild>
179
+ <Button variant="ghost" size="icon">
180
+ <FileStack />
181
+ </Button>
182
+ </TooltipTrigger>
183
+ <TooltipContent>
184
+
185
+ Available in: {item.multipleVersions.join(", ")}
186
+ </TooltipContent>
187
+ </Tooltip>
188
+ )}
189
+ </div>
190
+
191
+ </div>
192
+ </div>
193
+ )
194
+ })}
195
+ </div>
196
+ );
197
+ };
198
+
199
+ export default TableWithImageView;
@@ -79,12 +79,12 @@ const TableView: FC<TableViewProps> = ({ items }) => {
79
79
  <DropdownMenuContent>
80
80
  <DropdownMenuItem>
81
81
  <a href={item.files[fileKey].view} target="_blank" rel="noreferrer" className="flex items-center">
82
- <Eye className="mr-2 " /> Open
82
+ <Eye className="mr-2" /> Open {/*TODO: use i18n functions*/}
83
83
  </a>
84
84
  </DropdownMenuItem>
85
85
  <DropdownMenuItem>
86
86
  <a href={item.files[fileKey].download} target="_blank" rel="noreferrer" className="flex items-center">
87
- <CloudDownload className="mr-2 " /> Download
87
+ <CloudDownload className="mr-2" /> Download {/*TODO: use i18n functions*/}
88
88
  </a>
89
89
  </DropdownMenuItem>
90
90
  </DropdownMenuContent>
@@ -6,9 +6,10 @@ import {
6
6
  SidebarContent,
7
7
  } from "@c-rex/ui/sidebar";
8
8
  import { useTranslations } from "next-intl";
9
- import { articleInfoItemType, DocumentsType } from "@c-rex/types";
9
+ import { articleInfoItemType, DocumentsType, Favorite } from "@c-rex/types";
10
10
  import { InfoTable } from "./info/info-table";
11
11
  import { AvailableVersionsInterface } from "@c-rex/interfaces";
12
+ import { useFavoritesStore } from "./stores/favorites-store";
12
13
 
13
14
  interface SidebarProps extends ComponentProps<typeof Sidebar> {
14
15
  articleInfo: articleInfoItemType[],
@@ -16,11 +17,26 @@ interface SidebarProps extends ComponentProps<typeof Sidebar> {
16
17
  files: DocumentsType,
17
18
  articleAvailableVersions: AvailableVersionsInterface[],
18
19
  documentAvailableVersions: AvailableVersionsInterface[]
19
-
20
+ documentId: string
20
21
  }
21
22
 
22
- export function RightSidebar({ articleInfo, documentInfo, files, articleAvailableVersions, documentAvailableVersions, ...props }: SidebarProps) {
23
+ export function RightSidebar({
24
+ articleInfo,
25
+ documentInfo,
26
+ files,
27
+ articleAvailableVersions,
28
+ documentAvailableVersions,
29
+ documentId,
30
+ ...props
31
+ }: SidebarProps) {
23
32
  const t = useTranslations();
33
+ const favoritesDocuments = useFavoritesStore((state) => state.documents);
34
+ const isFavoriteDocument = Object.keys(favoritesDocuments).some((docId) => docId === documentId);
35
+ let topicList: Favorite[] = []
36
+
37
+ if (isFavoriteDocument && favoritesDocuments[documentId]) {
38
+ topicList = favoritesDocuments[documentId].topics
39
+ }
24
40
 
25
41
  return (
26
42
  <Sidebar {...props}>
@@ -34,6 +50,7 @@ export function RightSidebar({ articleInfo, documentInfo, files, articleAvailabl
34
50
  items={documentInfo}
35
51
  files={files}
36
52
  availableVersions={documentAvailableVersions}
53
+ markersList={isFavoriteDocument ? topicList : []}
37
54
  />
38
55
  )}
39
56
 
@@ -0,0 +1,82 @@
1
+ import { Favorite } from "@c-rex/types";
2
+ import { create } from "zustand";
3
+ import { persist } from "zustand/middleware";
4
+
5
+
6
+ type FavoritesStore = {
7
+ favorites: Favorite[];
8
+ documents: Record<string, { topics: Favorite[], label?: string }>;
9
+ favoriteTopic: (documentId: string, id: string, label: string, color: string) => void;
10
+ unfavoriteTopic: (documentId: string, id: string) => void;
11
+ favoriteDocument: (id: string, label: string) => void;
12
+ unfavoriteDocument: (id: string) => void;
13
+ };
14
+
15
+ export const useFavoritesStore = create<FavoritesStore>()(
16
+ persist((set) => ({
17
+ documents: {},
18
+ favorites: [],
19
+ favoriteTopic: (documentId: string, id: string, label: string, color: string) =>
20
+ set((state) => ({
21
+ documents: favoriteTopic(state.documents, documentId, id, label, color),
22
+ favorites: [...state.favorites, { id, label, color }, { id: documentId, label: "", color: "" }],
23
+ })),
24
+ unfavoriteTopic: (documentId: string, id: string) =>
25
+ set((state) => ({
26
+ documents: unfavoriteTopic(state.documents, documentId, id),
27
+ favorites: state.favorites.filter((topic) => topic.id !== id),
28
+ })),
29
+ favoriteDocument: (id: string, label: string) =>
30
+ set((state) => {
31
+ const documentsCopy = { ...state.documents };
32
+ if (documentsCopy[id]) {
33
+ return state;
34
+ }
35
+
36
+ return {
37
+ documents: {
38
+ ...state.documents,
39
+ [id]: { topics: [], label },
40
+ },
41
+ favorites: [...state.favorites, { id, label, color: "" }],
42
+ }
43
+ }),
44
+ unfavoriteDocument: (id: string) =>
45
+ set((state) => {
46
+ const documentsCopy = { ...state.documents };
47
+ delete documentsCopy[id];
48
+ return { documents: documentsCopy };
49
+ }),
50
+ }), {
51
+ name: "c-rex-favorites",
52
+ })
53
+ );
54
+
55
+
56
+ const favoriteTopic = (documents: Record<string, { topics: Favorite[] }>, documentId: string, id: string, label: string, color: string): Record<string, { topics: Favorite[] }> => {
57
+
58
+ const documentsCopy = { ...documents };
59
+ const notFound = documents[documentId] == undefined;
60
+
61
+ if (notFound) {
62
+ documentsCopy[documentId] = { topics: [] };
63
+ }
64
+
65
+ documentsCopy[documentId]!.topics.push({ id, label, color });
66
+
67
+ return documentsCopy
68
+ };
69
+
70
+ const unfavoriteTopic = (documents: Record<string, { topics: Favorite[] }>, documentId: string, id: string): Record<string, { topics: Favorite[] }> => {
71
+
72
+ const documentsCopy = { ...documents };
73
+ const notFound = documents[documentId] == undefined;
74
+
75
+ if (notFound) {
76
+ return documentsCopy;
77
+ }
78
+
79
+ documentsCopy[documentId]!.topics = documentsCopy[documentId]!.topics.filter(topic => topic.id !== id);
80
+
81
+ return documentsCopy
82
+ }
@@ -2,6 +2,7 @@ import { UI_LANG_KEY } from "@c-rex/constants";
2
2
  import { AVAILABLE_CONTENT_LANG_KEY } from "@c-rex/constants";
3
3
  import { CONTENT_LANG_KEY } from "@c-rex/constants";
4
4
  import { LanguageAndCountries } from "@c-rex/interfaces";
5
+ import { getCookie, setCookie } from "@c-rex/utils";
5
6
  import { create } from "zustand";
6
7
 
7
8
  type LanguageStoreType = {
@@ -14,33 +15,21 @@ type LanguageStoreType = {
14
15
  hydrate: () => void;
15
16
  };
16
17
 
17
- function setCookie(name: string, value: string, days = 365) {
18
- const expires = new Date(Date.now() + days * 864e5).toUTCString();
19
- document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
20
- }
21
-
22
- function getCookie(name: string): string {
23
- return document.cookie
24
- .split('; ')
25
- .find(row => row.startsWith(name + '='))
26
- ?.split('=')[1] || '';
27
- }
28
-
29
18
  export const useLanguageStore = create<LanguageStoreType>((set) => ({
30
19
  contentLang: "",
31
20
  uiLang: "",
32
21
  availableLanguages: [],
33
22
  setContentLang: (v) => {
34
23
  set({ contentLang: v });
35
- setCookie(CONTENT_LANG_KEY, v);
24
+ setCookie(CONTENT_LANG_KEY, v, 7);
36
25
  },
37
26
  setUiLang: (v) => {
38
27
  set({ uiLang: v });
39
- setCookie(UI_LANG_KEY, v);
28
+ setCookie(UI_LANG_KEY, v, 7);
40
29
  },
41
30
  setAvailableLanguages: (list) => {
42
31
  set({ availableLanguages: list });
43
- setCookie(AVAILABLE_CONTENT_LANG_KEY, JSON.stringify(list));
32
+ setCookie(AVAILABLE_CONTENT_LANG_KEY, JSON.stringify(list), 7);
44
33
  },
45
34
  hydrate: () => {
46
35
  set({
@@ -12,7 +12,7 @@ type SearchSettingsStore = {
12
12
 
13
13
  export const useSearchSettingsStore = create<SearchSettingsStore>()(
14
14
  persist((set) => ({
15
- language: [],
15
+ language: [] as string[],
16
16
  wildcard: "BOTH",
17
17
  operator: "OR",
18
18
  like: false,
@@ -1,39 +0,0 @@
1
- import React, { FC, JSX } from "react";
2
- import {
3
- DropdownMenu as DropdownMenuComp,
4
- DropdownMenuContent,
5
- DropdownMenuItem,
6
- DropdownMenuTrigger,
7
- } from "@c-rex/ui/dropdown-menu";
8
-
9
- interface DropdownMenuProps {
10
- items: {
11
- format: string;
12
- link: string;
13
- }[];
14
- icon: JSX.Element;
15
- }
16
-
17
- export const DropdownMenu: FC<DropdownMenuProps> = ({
18
- items, icon
19
- }) => {
20
-
21
- return (
22
- <DropdownMenuComp>
23
- <DropdownMenuTrigger className="mx-2">
24
- {icon}
25
- </DropdownMenuTrigger>
26
- <DropdownMenuContent>
27
- {items.map((file, index) => {
28
- return (
29
- <DropdownMenuItem key={index}>
30
- <a href={file.link} target="_blank" rel="noreferrer">
31
- {file.format}
32
- </a>
33
- </DropdownMenuItem>
34
- )
35
- })}
36
- </DropdownMenuContent>
37
- </DropdownMenuComp>
38
- );
39
- };