@c-rex/components 0.3.0-build.35 → 0.3.0-build.36

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.
Files changed (45) hide show
  1. package/package.json +28 -36
  2. package/src/article/article-action-bar.tsx +4 -1
  3. package/src/{check-article-lang.tsx → article/check-article-lang.tsx} +1 -1
  4. package/src/article/render-article-highlight.tsx +108 -0
  5. package/src/article/render-article.tsx +28 -0
  6. package/src/autocomplete.tsx +2 -2
  7. package/src/carousel/carousel.tsx +5 -2
  8. package/src/carousel/information-unit-carousel-item.tsx +1 -1
  9. package/src/content-unavailable.tsx +20 -0
  10. package/src/directoryNodes/directory-tree-context.tsx +9 -4
  11. package/src/documents/description-preview.tsx +14 -4
  12. package/src/documents/result-list-item.tsx +35 -46
  13. package/src/favorites/__tests__/favorites-hydration.test.tsx +245 -0
  14. package/src/favorites/bookmark-button.tsx +38 -20
  15. package/src/favorites/favorite-button.tsx +23 -24
  16. package/src/favorites/favorites-context.tsx +287 -0
  17. package/src/icons/file-icon.tsx +9 -26
  18. package/src/info/information-unit-metadata-grid-client.tsx +21 -21
  19. package/src/page-wrapper.tsx +1 -1
  20. package/src/renditions/html-client.tsx +8 -6
  21. package/src/renditions/html.tsx +3 -1
  22. package/src/restriction-menu/restriction-menu-item.tsx +48 -58
  23. package/src/restriction-menu/restriction-selection-command-menu.tsx +444 -0
  24. package/src/restriction-menu/restriction-selection-menu.tsx +3 -5
  25. package/src/restriction-menu/taxonomy-restriction-command-menu.tsx +111 -0
  26. package/src/restriction-menu/taxonomy-restriction-menu.tsx +1 -7
  27. package/src/results/filter-navbar.tsx +81 -76
  28. package/src/results/filter-sidebar/context.tsx +32 -0
  29. package/src/results/filter-sidebar/index.tsx +44 -35
  30. package/src/results/generic/search-results-client.tsx +5 -4
  31. package/src/results/generic/table-result-list.tsx +16 -16
  32. package/src/results/information-unit-search-results-card-list.tsx +4 -1
  33. package/src/results/pagination.tsx +43 -40
  34. package/src/search-input.tsx +4 -2
  35. package/src/toc/toc-browse-controls.tsx +2 -2
  36. package/src/toc/toc-tree-panel.tsx +19 -16
  37. package/src/article/article-content.tsx +0 -19
  38. package/src/breadcrumb.tsx +0 -124
  39. package/src/directoryNodes/tree-of-content.tsx +0 -68
  40. package/src/render-article.tsx +0 -75
  41. package/src/restriction-menu/restriction-menu-container.tsx +0 -4
  42. package/src/restriction-menu/restriction-menu.tsx +0 -4
  43. package/src/stores/__tests__/favorites-store.test.ts +0 -54
  44. package/src/stores/favorites-store.ts +0 -163
  45. /package/src/{render-article.module.css → article/render-article.module.css} +0 -0
@@ -1,13 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import { FC, useEffect, useMemo, useRef, useState } from "react";
4
+ import { useTranslations } from "next-intl";
4
5
  import { CommonItemsModel } from "@c-rex/interfaces";
5
6
  import { FileStack } from "lucide-react";
6
7
  import { cn, findRelatedFragmentShortId, generateQueryParams } from "@c-rex/utils";
7
8
  import { Button } from "@c-rex/ui/button";
8
9
  import { Badge } from "@c-rex/ui/badge";
9
10
  import { Tooltip, TooltipContent, TooltipTrigger } from "@c-rex/ui/tooltip";
10
- import { RESULT_TYPES } from "@c-rex/constants";
11
11
  import { FileDownloadDropdown } from "@c-rex/components/file-download";
12
12
  import { FavoriteButton } from "@c-rex/components/favorite-button";
13
13
  import { ImageRenditionContainer } from "../renditions/image/container";
@@ -34,10 +34,8 @@ type RowContentProps = {
34
34
  item: CommonItemsModel;
35
35
  itemLink: string;
36
36
  title: string;
37
- language: string;
38
37
  itemType: ResultTypes;
39
38
  multipleVersions: string[];
40
- isDocument: boolean;
41
39
  isLast: boolean;
42
40
  previewFragmentShortId?: string;
43
41
  descriptionFragmentShortId?: string;
@@ -48,15 +46,15 @@ const DocumentResultListRowContent: FC<RowContentProps> = ({
48
46
  item,
49
47
  itemLink,
50
48
  title,
51
- language,
52
49
  itemType,
53
50
  multipleVersions,
54
- isDocument,
55
51
  isLast,
56
52
  previewFragmentShortId,
57
53
  descriptionFragmentShortId,
58
54
  isLoadingDetails = false,
59
55
  }) => {
56
+ const t = useTranslations();
57
+
60
58
  return (
61
59
  <div
62
60
  className={cn(
@@ -65,14 +63,14 @@ const DocumentResultListRowContent: FC<RowContentProps> = ({
65
63
  `c-rex-result-item c-rex-result-${itemType}`
66
64
  )}
67
65
  >
68
- <div className="w-20 sm:w-24 flex shrink-0 items-start justify-center pt-1">
66
+ <div className="w-20 sm:w-24 flex shrink-0 justify-center pt-1">
69
67
  {isLoadingDetails ? (
70
68
  <Skeleton className="w-20 sm:w-24 aspect-[210/297]" />
71
69
  ) : (
72
- <Link href={itemLink} className="block w-20 sm:w-24">
70
+ <Link href={itemLink} className="w-full h-full block">
73
71
  <ImageRenditionContainer
74
72
  fragmentShortId={previewFragmentShortId}
75
- emptyImageStyle="w-20 sm:w-24 aspect-[210/297]"
73
+ emptyImageStyle="w-full h-full"
76
74
  imageStyle="block w-full h-auto max-w-full object-contain object-top mx-auto"
77
75
  />
78
76
  </Link>
@@ -81,11 +79,11 @@ const DocumentResultListRowContent: FC<RowContentProps> = ({
81
79
 
82
80
  <div className="flex-1 p-2 flex flex-col justify-start">
83
81
  <span className="text-sm text-muted-foreground">
84
- {item.revision} - {language}
82
+ {item.revision}
85
83
  </span>
86
84
 
87
85
  <span className="text-lg font-medium">
88
- <a className="hover:underline" href={itemLink}>{title}</a>
86
+ <a className="hover:underline [overflow-wrap:anywhere] hyphens-auto" href={itemLink}>{title}</a>
89
87
  </span>
90
88
 
91
89
  <div>
@@ -109,36 +107,32 @@ const DocumentResultListRowContent: FC<RowContentProps> = ({
109
107
  </span>
110
108
  </div>
111
109
 
112
- <div className="flex flex-col p-2 ml-auto justify-start self-start">
113
- <div className="flex gap-2">
114
- <FileDownloadDropdown renditions={item.renditions} />
115
-
116
- <FavoriteButton
117
- id={item.shortId!}
118
- type={itemType}
119
- label={title}
120
- />
121
-
122
- {multipleVersions.length > 1 && (
123
- <Tooltip>
124
- <TooltipTrigger asChild>
125
- <Button variant="ghost" size="icon">
126
- <FileStack />
127
- </Button>
128
- </TooltipTrigger>
129
- <TooltipContent>
130
- Available in: {multipleVersions.join(", ")}
131
- </TooltipContent>
132
- </Tooltip>
133
- )}
110
+ <div className="flex flex-row p-2 ml-auto justify-between self-start gap-2 w-full sm:justify-start sm:w-auto">
111
+ <FileDownloadDropdown renditions={item.renditions} />
112
+
113
+ <FavoriteButton
114
+ id={item.shortId!}
115
+ type={itemType}
116
+ label={title}
117
+ />
118
+
119
+ {multipleVersions.length > 1 && (
120
+ <Tooltip>
121
+ <TooltipTrigger asChild>
122
+ <Button variant="ghost" size="icon">
123
+ <FileStack />
124
+ </Button>
125
+ </TooltipTrigger>
126
+ <TooltipContent>
127
+ {t("availableIn")}: {multipleVersions.join(", ")}
128
+ </TooltipContent>
129
+ </Tooltip>
130
+ )}
134
131
 
135
- {isDocument && (
136
- <BookmarkButton
137
- shortId={item.shortId!}
138
- triggerVariant="ghost"
139
- />
140
- )}
141
- </div>
132
+ <BookmarkButton
133
+ shortId={item.shortId!}
134
+ triggerVariant="ghost"
135
+ />
142
136
  </div>
143
137
  </div>
144
138
  );
@@ -152,8 +146,7 @@ export const DocumentsResultListItem: FC<Props> = ({
152
146
  isLast,
153
147
  eager = false,
154
148
  }) => {
155
- const { title, language, itemType, multipleVersions, packageId } = getResultItemSummary(item);
156
- const isDocument = itemType === RESULT_TYPES.DOCUMENT;
149
+ const { title, itemType, multipleVersions, packageId } = getResultItemSummary(item);
157
150
  const [canLoadDetails, setCanLoadDetails] = useState(eager);
158
151
  const rowRef = useRef<HTMLDivElement | null>(null);
159
152
  const queryParams: QueryParams[] = [];
@@ -199,14 +192,12 @@ export const DocumentsResultListItem: FC<Props> = ({
199
192
  item={item}
200
193
  itemLink={itemLink}
201
194
  title={title}
202
- language={language}
203
195
  itemType={itemType}
204
196
  multipleVersions={multipleVersions}
205
- isDocument={isDocument}
206
197
  isLast={isLast}
207
198
  isLoadingDetails={!canLoadDetails}
208
199
  />
209
- ), [canLoadDetails, isDocument, isLast, item, itemLink, itemType, language, multipleVersions, title]);
200
+ ), [canLoadDetails, isLast, item, itemLink, itemType, multipleVersions, title]);
210
201
 
211
202
  return (
212
203
  <div ref={rowRef}>
@@ -238,10 +229,8 @@ export const DocumentsResultListItem: FC<Props> = ({
238
229
  item={item}
239
230
  itemLink={itemLink}
240
231
  title={title}
241
- language={language}
242
232
  itemType={itemType}
243
233
  multipleVersions={multipleVersions}
244
- isDocument={isDocument}
245
234
  isLast={isLast}
246
235
  previewFragmentShortId={previewFragmentShortId}
247
236
  descriptionFragmentShortId={descriptionFragmentShortId}
@@ -0,0 +1,245 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import React, { useEffect } from "react";
4
+ import { TextDecoder, TextEncoder } from "util";
5
+ import { act } from "react";
6
+ import { createRoot, hydrateRoot } from "react-dom/client";
7
+ import { FavoriteButton } from "../favorite-button";
8
+ import { BookmarkButton } from "../bookmark-button";
9
+ import {
10
+ FAVORITES_STORAGE_KEY,
11
+ FavoritesProvider,
12
+ type FavoritesState,
13
+ useFavorites,
14
+ } from "../favorites-context";
15
+
16
+ global.TextEncoder = TextEncoder;
17
+ global.TextDecoder = TextDecoder as typeof global.TextDecoder;
18
+ (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
19
+
20
+ const { renderToString } = require("react-dom/server");
21
+
22
+ jest.mock("next-intl", () => ({
23
+ useTranslations: () => (key: string) => {
24
+ const messages: Record<string, string> = {
25
+ title: "Bookmarks",
26
+ description: "Manage your bookmarks here",
27
+ empty: "No bookmarks yet.",
28
+ };
29
+
30
+ return messages[key] ?? key;
31
+ },
32
+ }));
33
+
34
+ jest.mock("@c-rex/utils", () => ({
35
+ getLocalStorageJson: (key: string) => {
36
+ const value = window.localStorage.getItem(key);
37
+ return value ? JSON.parse(value) : null;
38
+ },
39
+ setLocalStorageJson: (key: string, value: unknown) => {
40
+ window.localStorage.setItem(key, JSON.stringify(value));
41
+ },
42
+ removeLocalStorageItem: (key: string) => {
43
+ window.localStorage.removeItem(key);
44
+ },
45
+ }));
46
+
47
+ jest.mock("next/link", () => ({
48
+ __esModule: true,
49
+ default: ({ children, href, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
50
+ <a href={href} {...props}>{children}</a>
51
+ ),
52
+ }));
53
+
54
+ jest.mock("@c-rex/ui/button", () => ({
55
+ Button: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
56
+ <button {...props}>{children}</button>
57
+ ),
58
+ }));
59
+
60
+ jest.mock("@c-rex/ui/dialog", () => ({
61
+ Dialog: ({ children }: { children: React.ReactNode }) => <div data-testid="dialog">{children}</div>,
62
+ DialogTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
63
+ DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
64
+ DialogHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
65
+ DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
66
+ DialogDescription: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
67
+ }));
68
+
69
+ jest.mock("@c-rex/ui/table", () => ({
70
+ Table: ({ children }: { children: React.ReactNode }) => <table>{children}</table>,
71
+ TableBody: ({ children }: { children: React.ReactNode }) => <tbody>{children}</tbody>,
72
+ TableRow: ({ children, ...props }: React.HTMLAttributes<HTMLTableRowElement>) => <tr {...props}>{children}</tr>,
73
+ TableCell: ({ children, ...props }: React.TdHTMLAttributes<HTMLTableCellElement>) => <td {...props}>{children}</td>,
74
+ }));
75
+
76
+ jest.mock("sonner", () => ({
77
+ toast: {
78
+ error: jest.fn(),
79
+ },
80
+ }));
81
+
82
+ const persistedState: FavoritesState = {
83
+ favorites: [
84
+ { id: "doc-1", label: "Document 1", color: "" },
85
+ { id: "topic-1", label: "Topic 1", color: "red-500" },
86
+ ],
87
+ documents: {
88
+ "doc-1": {
89
+ label: "Document 1",
90
+ topics: [{ id: "topic-1", label: "Topic 1", color: "red-500" }],
91
+ },
92
+ },
93
+ };
94
+
95
+ const ProviderProbe = () => {
96
+ const isHydrated = useFavorites((state) => state.isHydrated);
97
+ const favoritesCount = useFavorites((state) => state.favorites.length);
98
+
99
+ return (
100
+ <div
101
+ data-hydrated={String(isHydrated)}
102
+ data-favorites-count={String(favoritesCount)}
103
+ />
104
+ );
105
+ };
106
+
107
+ const EffectProbe = ({ onHydrated }: { onHydrated: (payload: { hydrated: boolean; count: number }) => void }) => {
108
+ const isHydrated = useFavorites((state) => state.isHydrated);
109
+ const favoritesCount = useFavorites((state) => state.favorites.length);
110
+
111
+ useEffect(() => {
112
+ onHydrated({ hydrated: isHydrated, count: favoritesCount });
113
+ }, [favoritesCount, isHydrated, onHydrated]);
114
+
115
+ return null;
116
+ };
117
+
118
+ describe("favorites hydration", () => {
119
+ beforeEach(() => {
120
+ window.localStorage.clear();
121
+ jest.clearAllMocks();
122
+ });
123
+
124
+ it("keeps server markup stable before hydration and loads persisted state after mount", async () => {
125
+ window.localStorage.setItem(FAVORITES_STORAGE_KEY, JSON.stringify(persistedState));
126
+
127
+ const serverMarkup = renderToString(
128
+ <FavoritesProvider>
129
+ <ProviderProbe />
130
+ </FavoritesProvider>
131
+ );
132
+
133
+ expect(serverMarkup).toContain('data-hydrated="false"');
134
+ expect(serverMarkup).toContain('data-favorites-count="0"');
135
+
136
+ const container = document.createElement("div");
137
+ container.innerHTML = serverMarkup;
138
+
139
+ await act(async () => {
140
+ hydrateRoot(
141
+ container,
142
+ <FavoritesProvider>
143
+ <ProviderProbe />
144
+ </FavoritesProvider>
145
+ );
146
+ });
147
+
148
+ const probe = container.querySelector("div[data-hydrated]");
149
+
150
+ expect(probe?.getAttribute("data-hydrated")).toBe("true");
151
+ expect(probe?.getAttribute("data-favorites-count")).toBe("2");
152
+ });
153
+
154
+ it("renders favorite button as loading on the server and shows persisted favorite after hydration", async () => {
155
+ window.localStorage.setItem(FAVORITES_STORAGE_KEY, JSON.stringify(persistedState));
156
+
157
+ const serverMarkup = renderToString(
158
+ <FavoritesProvider>
159
+ <FavoriteButton id="doc-1" type="DOCUMENT" label="Document 1" />
160
+ </FavoritesProvider>
161
+ );
162
+
163
+ expect(serverMarkup).toContain("Loading favorites");
164
+ expect(serverMarkup).not.toContain("Remove favorite");
165
+
166
+ const container = document.createElement("div");
167
+ document.body.appendChild(container);
168
+ const root = createRoot(container);
169
+
170
+ await act(async () => {
171
+ root.render(
172
+ <FavoritesProvider>
173
+ <FavoriteButton id="doc-1" type="DOCUMENT" label="Document 1" />
174
+ </FavoritesProvider>
175
+ );
176
+ });
177
+
178
+ const button = container.querySelector("button");
179
+
180
+ expect(button?.getAttribute("aria-label")).toBe("Remove favorite");
181
+ expect(button?.hasAttribute("disabled")).toBe(false);
182
+
183
+ await act(async () => {
184
+ root.unmount();
185
+ });
186
+ });
187
+
188
+ it("hides bookmark count on the server and reveals persisted bookmarks after hydration", async () => {
189
+ window.localStorage.setItem(FAVORITES_STORAGE_KEY, JSON.stringify(persistedState));
190
+
191
+ const serverMarkup = renderToString(
192
+ <FavoritesProvider>
193
+ <BookmarkButton shortId="doc-1" />
194
+ </FavoritesProvider>
195
+ );
196
+
197
+ expect(serverMarkup).not.toContain(">1<");
198
+ expect(serverMarkup).toContain("Loading...");
199
+
200
+ const container = document.createElement("div");
201
+ document.body.appendChild(container);
202
+ const root = createRoot(container);
203
+
204
+ await act(async () => {
205
+ root.render(
206
+ <FavoritesProvider>
207
+ <BookmarkButton shortId="doc-1" />
208
+ </FavoritesProvider>
209
+ );
210
+ });
211
+
212
+ expect(container.textContent).toContain("1");
213
+ expect(container.textContent).toContain("Topic 1");
214
+ expect(container.textContent).not.toContain("Loading...");
215
+
216
+ await act(async () => {
217
+ root.unmount();
218
+ });
219
+ });
220
+
221
+ it("marks the provider as hydrated and exposes persisted favorites to consumers after mount", async () => {
222
+ window.localStorage.setItem(FAVORITES_STORAGE_KEY, JSON.stringify(persistedState));
223
+ const onHydrated = jest.fn();
224
+ const container = document.createElement("div");
225
+ document.body.appendChild(container);
226
+ const root = createRoot(container);
227
+
228
+ await act(async () => {
229
+ root.render(
230
+ <FavoritesProvider>
231
+ <EffectProbe onHydrated={onHydrated} />
232
+ </FavoritesProvider>
233
+ );
234
+ });
235
+
236
+ expect(onHydrated).toHaveBeenLastCalledWith({
237
+ hydrated: true,
238
+ count: 2,
239
+ });
240
+
241
+ await act(async () => {
242
+ root.unmount();
243
+ });
244
+ });
245
+ });
@@ -2,27 +2,31 @@
2
2
 
3
3
  import { ComponentProps, FC } from "react";
4
4
  import { Button } from "@c-rex/ui/button";
5
- import { Trash } from "lucide-react";
6
- import { cn } from "@c-rex/utils";
5
+ import { Loader2, Trash } from "lucide-react";
7
6
  import Link from "next/link";
8
- import { useFavoritesStore } from "../stores/favorites-store";
9
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@c-rex/ui/dialog";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ DialogTrigger
14
+ } from "@c-rex/ui/dialog";
10
15
  import { useTranslations } from "next-intl";
11
16
  import {
12
17
  Table,
13
18
  TableBody,
14
19
  TableCell,
15
20
  TableRow,
16
- } from "@c-rex/ui/table"
21
+ } from "@c-rex/ui/table";
17
22
  import { FaRegBookmark } from "react-icons/fa6";
18
-
19
- const EMPTY_TOPICS: { id: string; label: string; color: string }[] = [];
23
+ import { useFavorites } from "./favorites-context";
20
24
 
21
25
  type BookmarkProps = {
22
26
  shortId: string;
23
27
  linkPattern?: string;
24
28
  triggerVariant?: ComponentProps<typeof Button>["variant"];
25
- }
29
+ };
26
30
 
27
31
  export const BookmarkButton: FC<BookmarkProps> = ({
28
32
  shortId,
@@ -30,35 +34,49 @@ export const BookmarkButton: FC<BookmarkProps> = ({
30
34
  triggerVariant = "outline"
31
35
  }) => {
32
36
  const t = useTranslations("bookmarks");
33
- const removeFavoriteTopic = useFavoritesStore((state) => state.unfavoriteTopic);
34
- const documentLabel = useFavoritesStore((state) => state.documents[shortId]?.label);
35
- const markersList = useFavoritesStore((state) => state.documents[shortId]?.topics) ?? EMPTY_TOPICS;
37
+ const tRoot = useTranslations();
38
+ const removeFavoriteTopic = useFavorites((state) => state.unfavoriteTopic);
39
+ const documentLabel = useFavorites((state) => state.documents[shortId]?.label);
40
+ const markersList = useFavorites((state) => state.documents[shortId]?.topics) ?? [];
41
+ const isHydrated = useFavorites((state) => state.isHydrated);
42
+ const visibleMarkersCount = isHydrated ? markersList.length : 0;
43
+
36
44
  return (
37
45
  <Dialog>
38
46
  <DialogTrigger asChild>
39
- <Button variant={triggerVariant} size="icon" className="relative">
47
+ <Button variant={triggerVariant} size="icon" className="relative" aria-label={t("title")}>
40
48
  <FaRegBookmark className="text-primary !size-4" />
41
49
 
42
- {markersList.length > 0 && (
50
+ {isHydrated && visibleMarkersCount > 0 && (
43
51
  <span
44
52
  className="absolute -top-[10px] -right-[10px] min-w-5 min-h-5 bg-primary text-white rounded-full"
45
53
  >
46
- {markersList.length}
54
+ {visibleMarkersCount}
47
55
  </span>
48
56
  )}
49
57
  </Button>
50
-
51
58
  </DialogTrigger>
52
59
  <DialogContent>
53
60
  <DialogHeader>
54
61
  <DialogTitle>{t("title")}</DialogTitle>
55
62
  <DialogDescription>
56
- {documentLabel || t("description")}
63
+ {isHydrated ? (documentLabel || t("description")) : t("description")}
57
64
  </DialogDescription>
58
65
  </DialogHeader>
59
66
  <Table>
60
67
  <TableBody>
61
- {markersList.length === 0 && (
68
+ {!isHydrated && (
69
+ <TableRow>
70
+ <TableCell colSpan={3} className="text-center">
71
+ <span className="inline-flex items-center gap-2">
72
+ <Loader2 className="size-4 animate-spin" />
73
+ {tRoot("loading")}
74
+ </span>
75
+ </TableCell>
76
+ </TableRow>
77
+ )}
78
+
79
+ {isHydrated && visibleMarkersCount === 0 && (
62
80
  <TableRow>
63
81
  <TableCell colSpan={3} className="text-center">
64
82
  {t("empty")}
@@ -66,7 +84,7 @@ export const BookmarkButton: FC<BookmarkProps> = ({
66
84
  </TableRow>
67
85
  )}
68
86
 
69
- {markersList.map((item) => (
87
+ {isHydrated && markersList.map((item) => (
70
88
  <TableRow key={item.id} className="min-h-12">
71
89
  <TableCell>
72
90
  <FaRegBookmark className={`text-${item.color}`} />
@@ -95,5 +113,5 @@ export const BookmarkButton: FC<BookmarkProps> = ({
95
113
  </Table>
96
114
  </DialogContent>
97
115
  </Dialog>
98
- )
99
- }
116
+ );
117
+ };
@@ -3,7 +3,7 @@
3
3
  import { FC, useState } from "react";
4
4
  import { Button } from "@c-rex/ui/button";
5
5
  import { FaStar, FaRegStar } from "react-icons/fa";
6
- import { useFavoritesStore } from "../stores/favorites-store";
6
+ import { useFavorites } from "./favorites-context";
7
7
  import { MARKER_COLORS, RESULT_TYPES } from "@c-rex/constants";
8
8
  import { ResultTypes } from "@c-rex/types";
9
9
  import { Loader2 } from "lucide-react";
@@ -14,14 +14,16 @@ export const FavoriteButton: FC<{
14
14
  type: ResultTypes;
15
15
  label: string;
16
16
  }> = ({ id, type, label }) => {
17
- const addFavoriteTopic = useFavoritesStore((state) => state.favoriteTopic);
18
- const addFavoriteDocument = useFavoritesStore((state) => state.favoriteDocument);
19
- const removeFavoriteTopic = useFavoritesStore((state) => state.unfavoriteTopic);
20
- const removeFavoriteDocument = useFavoritesStore((state) => state.unfavoriteDocument);
21
-
22
- const favoriteDocumentList = useFavoritesStore((state) => state.documents);
23
- const favoriteList = useFavoritesStore((state) => state.favorites);
17
+ const addFavoriteTopic = useFavorites((state) => state.favoriteTopic);
18
+ const addFavoriteDocument = useFavorites((state) => state.favoriteDocument);
19
+ const removeFavoriteTopic = useFavorites((state) => state.unfavoriteTopic);
20
+ const removeFavoriteDocument = useFavorites((state) => state.unfavoriteDocument);
21
+
22
+ const favoriteDocumentList = useFavorites((state) => state.documents);
23
+ const favoriteList = useFavorites((state) => state.favorites);
24
+ const isHydrated = useFavorites((state) => state.isHydrated);
24
25
  const isFavorite = favoriteList.find((fav) => fav.id === id);
26
+
25
27
  const [documentData, setDocumentData] = useState<{ id: string, label: string } | null>(null);
26
28
  const [topicData, setTopicData] = useState<{ id: string, label: string } | null>(null);
27
29
  const [isLoading, setIsLoading] = useState(false);
@@ -109,7 +111,7 @@ export const FavoriteButton: FC<{
109
111
  };
110
112
 
111
113
  const handleToggle = async () => {
112
- if (isLoading) return;
114
+ if (!isHydrated || isLoading) return;
113
115
  setIsLoading(true);
114
116
 
115
117
  try {
@@ -125,25 +127,22 @@ export const FavoriteButton: FC<{
125
127
  }
126
128
  };
127
129
 
128
- if (isFavorite) {
129
- return (
130
- <Button variant="ghost" size="icon" onClick={handleToggle} disabled={isLoading}>
131
- {isLoading ? (
132
- <Loader2 className="animate-spin" />
133
- ) : (
134
- <FaStar className="color-primary" />
135
- )}
136
- </Button>
137
- );
138
- }
130
+ const showLoading = !isHydrated || isLoading;
139
131
 
140
132
  return (
141
- <Button variant="ghost" size="icon" onClick={handleToggle} disabled={isLoading}>
142
- {isLoading ? (
133
+ <Button
134
+ variant="ghost"
135
+ size="icon"
136
+ onClick={handleToggle}
137
+ disabled={showLoading}
138
+ >
139
+ {showLoading ? (
143
140
  <Loader2 className="animate-spin" />
141
+ ) : isFavorite ? (
142
+ <FaStar className="color-primary" />
144
143
  ) : (
145
144
  <FaRegStar />
146
145
  )}
147
146
  </Button>
148
- )
149
- }
147
+ );
148
+ };