@c-rex/components 0.1.16 → 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.16",
3
+ "version": "0.1.17",
4
4
  "files": [
5
5
  "src"
6
6
  ],
@@ -105,6 +105,10 @@
105
105
  "types": "./src/loading.tsx",
106
106
  "import": "./src/loading.tsx"
107
107
  },
108
+ "./favorite-button": {
109
+ "types": "./src/favorite-button.tsx",
110
+ "import": "./src/favorite-button.tsx"
111
+ },
108
112
  "./language-store": {
109
113
  "types": "./src/stores/language-store.ts",
110
114
  "import": "./src/stores/language-store.ts"
@@ -112,6 +116,10 @@
112
116
  "./highlight-store": {
113
117
  "types": "./src/stores/highlight-store.ts",
114
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"
115
123
  }
116
124
  },
117
125
  "scripts": {
@@ -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
+ }
@@ -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
+ }
@@ -15,8 +15,7 @@ import {
15
15
  DropdownMenuItem,
16
16
  DropdownMenuTrigger,
17
17
  } from "@c-rex/ui/dropdown-menu";
18
- import { Bookmark, CloudDownload, Eye } from "lucide-react";
19
-
18
+ import { CloudDownload, Eye } from "lucide-react";
20
19
  import { AvailableVersionsInterface } from "@c-rex/interfaces";
21
20
  import { Flag } from "../flag";
22
21
  import { Button } from "@c-rex/ui/button";
@@ -31,7 +30,8 @@ type Props = {
31
30
  markersList?: Favorite[]
32
31
  }
33
32
 
34
- export const InfoTable: FC<Props> = ({ title, items, files, availableVersions, markersList }) => {
33
+
34
+ export const InfoTable: FC<Props> = ({ title, items, files, availableVersions }) => {
35
35
  const t = useTranslations();
36
36
  const uiLang = useLanguageStore(state => state.uiLang);
37
37
  const newItems = filteredItems(items)
@@ -39,7 +39,9 @@ export const InfoTable: FC<Props> = ({ title, items, files, availableVersions, m
39
39
  return (
40
40
  <Card className="p-0 !pt-4">
41
41
  <CardHeader>
42
- <CardTitle className="text-lg">{title}</CardTitle>
42
+ <CardTitle className="text-lg flex justify-between">
43
+ {title}
44
+ </CardTitle>
43
45
  </CardHeader>
44
46
  <CardContent className="space-y-3 !p-0">
45
47
  <Table>
@@ -61,6 +63,25 @@ export const InfoTable: FC<Props> = ({ title, items, files, availableVersions, m
61
63
  </TableRow>
62
64
  ))}
63
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
+
64
85
  {files && (
65
86
  <TableRow className="min-h-12">
66
87
  <TableCell className="font-medium w-28 pl-4">
@@ -98,60 +119,6 @@ export const InfoTable: FC<Props> = ({ title, items, files, availableVersions, m
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
- {markersList && markersList.length > 0 && (
122
- <TableRow className="min-h-12">
123
- <TableCell className="font-medium w-28 pl-4">
124
- <h4 className="text-sm font-medium">Bookmarks</h4>
125
- </TableCell>
126
- <TableCell className="text-xs text-muted-foreground flex items-center gap-2 min-h-12">
127
- <DropdownMenu>
128
- <DropdownMenuTrigger className="mr-2" asChild>
129
- <Button variant="outline" size="icon" className="relative">
130
- <Bookmark className="text-primary" />
131
-
132
- <span
133
- className="absolute -top-[10px] -right-[10px] min-w-5 min-h-5 bg-primary text-white rounded-full"
134
- >{markersList.length}</span>
135
- </Button>
136
- </DropdownMenuTrigger>
137
- <DropdownMenuContent>
138
-
139
- {markersList.map((marker) => (
140
- <DropdownMenuItem key={marker.id}>
141
- <a href={`./${marker.id}`} target="_blank" rel="noreferrer" className="flex items-center">
142
- {marker.label}
143
- </a>
144
- </DropdownMenuItem>
145
- ))}
146
-
147
- </DropdownMenuContent>
148
- </DropdownMenu>
149
-
150
- </TableCell>
151
- </TableRow>
152
- )}
153
-
154
-
155
122
  </TableBody>
156
123
  </Table>
157
124
  </CardContent>
@@ -5,7 +5,7 @@ import { getServerSession } from "next-auth";
5
5
  import { SettingsMenu } from "./settings";
6
6
  import { UserMenu } from "./user-menu";
7
7
  import { SearchInput } from "./search-input";
8
- import { getClientConfig, getServerConfig } from "@c-rex/core";
8
+ import { CrexSDK } from "@c-rex/core/sdk";
9
9
 
10
10
  interface NavBarProps {
11
11
  title: string;
@@ -14,8 +14,9 @@ interface NavBarProps {
14
14
  }
15
15
 
16
16
  export const NavBar: FC<NavBarProps> = async ({ title, showInput, showPkgFilter }) => {
17
- const clientConfigs = getClientConfig();
18
- const serverConfigs = getServerConfig();
17
+ const sdk = new CrexSDK();
18
+ const clientConfigs = sdk.getClientConfig();
19
+ const serverConfigs = sdk.getServerConfig();
19
20
 
20
21
  let session: any;
21
22
  if (clientConfigs.OIDC.userEnabled) {
@@ -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 min-h-[70vh]">
64
+ <main ref={containerRef} lang={contentLang} className="pb-4 pr-4 min-h-[70vh]">
65
65
  {highlightedContent}
66
66
  </main>
67
67
  );
@@ -32,7 +32,7 @@ export const ResultList: FC<ResultListProps> = ({ items, pagination }: ResultLis
32
32
 
33
33
  return (
34
34
  <>
35
- {listComponent[configs.results.resultViewStyle]}
35
+ {listComponent[configs.results.resultViewStyle as ResultViewStyles]}
36
36
 
37
37
  <Pagination
38
38
  totalPages={pagination.pageCount}
@@ -1,7 +1,6 @@
1
1
  import { FC, useEffect, useState } from "react";
2
2
  import { informationUnitsResponseItem } from "@c-rex/interfaces";
3
3
  import { CloudDownload, Eye, FileStack, ImageOff } from "lucide-react";
4
- import { FaFilePdf } from "react-icons/fa6";
5
4
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@c-rex/ui/dropdown-menu";
6
5
  import { cn } from "@c-rex/utils";
7
6
  import { useQueryState } from "nuqs";
@@ -9,16 +8,16 @@ import { Skeleton } from "@c-rex/ui/skeleton";
9
8
  import { FavoriteButton } from "../favorite-button";
10
9
  import { Button } from "@c-rex/ui/button";
11
10
  import { Tooltip, TooltipContent, TooltipTrigger } from "@c-rex/ui/tooltip";
12
- import { ResultTypes } from "@c-rex/types";
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";
13
16
 
14
17
  interface TableWithImageViewProps {
15
18
  items: informationUnitsResponseItem[];
16
19
  }
17
20
 
18
- const IconsToFileExtension: Record<string, React.ReactNode> = {
19
- "application/pdf": <FaFilePdf className="h-5 w-5" />,
20
- };
21
-
22
21
  const Image: FC<{ id: string }> = ({ id }) => {
23
22
  const [imageUrl, setImageUrl] = useState("");
24
23
  const [loading, setLoading] = useState(true);
@@ -72,101 +71,127 @@ const Image: FC<{ id: string }> = ({ id }) => {
72
71
  );
73
72
  }
74
73
 
75
- const TableWithImageView: FC<TableWithImageViewProps> = ({ items, params }) => {
74
+ const TableWithImageView: FC<TableWithImageViewProps> = ({ items }) => {
76
75
  const [query] = useQueryState("search");
77
76
 
77
+ const favoritesDocuments = useFavoritesStore((state) => state.documents);
78
+
78
79
  return (
79
80
  <div className="mb-6">
80
- {items.map((item, index) => (
81
- <div
82
- className={cn(
83
- "min-h-12 flex flex-wrap items-center border px-4 py-2 gap-2 rounded mb-2",
84
- `c-rex-result-item c-rex-result-${item.type.toLowerCase()}`,
85
- item.disabled && "c-rex-result-item-disabled",
86
- )}
87
- key={index}
88
- >
89
-
90
- <div className="h-16 w-16 flex items-center">
91
- <Image id={item.shortId} {...params} />
92
- </div>
81
+ {items.map((item, index) => {
82
+ const isFavoriteDocument = Object.keys(favoritesDocuments).some((docId) => docId === item.shortId);
83
+ let topicList: Favorite[] = []
93
84
 
94
- <div className="flex-1 p-2 flex flex-col">
95
- <span className=" text-sm text-muted-foreground">
96
- {item.language.split("-")[0]?.toUpperCase()}
97
- </span>
98
-
99
- <span className="text-lg font-medium">
100
- {item.disabled ? (
101
- item.title
102
- ) : (
103
- <a className="hover:underline" href={`${item.link}?q=${query}`}>{item.title}</a>
104
- )}
105
- </span>
106
-
107
- <span className="text-sm">
108
- {item.type}
109
- </span>
110
- <span className="text-sm">
111
- short description here {/*
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 {/*
112
120
  TODO: get abstract if has the value key on it. Same behavior on class props
113
121
 
114
122
  abstract and description are not being returned by the API yet
115
123
  */}
116
- </span>
117
- </div>
124
+ </span>
125
+ </div>
118
126
 
119
- <div className="flex flex-col p-2 ml-auto justify-center">
120
- <span className="text-end text-sm text-muted-foreground mb-2">
121
- {item.revision}
122
- </span>
123
- <div className="flex gap-2">
124
- {Object.keys(item.files).map((fileKey, index) => {
125
- if (!item.files[fileKey]) return null
126
-
127
- return (
128
- <DropdownMenu key={index}>
129
- <DropdownMenuTrigger>
130
- <Button variant="ghost" size="icon" rounded="full">
131
- {IconsToFileExtension[fileKey]}
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>
132
169
  </Button>
133
- </DropdownMenuTrigger>
134
- <DropdownMenuContent>
135
- <DropdownMenuItem>
136
- <a href={item.files[fileKey].view} target="_blank" rel="noreferrer" className="flex items-center">
137
- <Eye className="mr-2" /> Open {/*TODO: use i18n functions*/}
138
- </a>
139
- </DropdownMenuItem>
140
- <DropdownMenuItem>
141
- <a href={item.files[fileKey].download} target="_blank" rel="noreferrer" className="flex items-center">
142
- <CloudDownload className="mr-2" /> Download {/*TODO: use i18n functions*/}
143
- </a>
144
- </DropdownMenuItem>
145
- </DropdownMenuContent>
146
- </DropdownMenu>
147
- )
148
- })}
149
-
150
- <FavoriteButton id={item.shortId} type={item.type as ResultTypes} label={item.title} />
151
-
152
- {item.multipleVersions.length > 1 && (
153
- <Tooltip>
154
- <TooltipTrigger asChild>
155
- <Button variant="ghost" size="icon" rounded="full">
156
- <FileStack /> {JSON.stringify(item.multipleVersions)}
157
- </Button>
158
- </TooltipTrigger>
159
- <TooltipContent>
160
-
161
- Available in: {item.multipleVersions}
162
- </TooltipContent>
163
- </Tooltip>
164
- )}
165
- </div>
170
+ }
171
+ />
172
+ )}
166
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>
167
192
  </div>
168
- </div>
169
- ))}
193
+ )
194
+ })}
170
195
  </div>
171
196
  );
172
197
  };
@@ -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
+ }