@c-rex/components 0.1.32 → 0.1.33
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
|
@@ -2,16 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
- Component: packages/components/src/favorites/bookmark-button.tsx
|
|
4
4
|
- Type: Client component
|
|
5
|
-
- Detected signals: useQueryStates=no, stores=
|
|
5
|
+
- Detected signals: useQueryStates=no, stores=yes, window/document=no, effects=no, fetch/call=no
|
|
6
6
|
|
|
7
7
|
## Possible re-render causes
|
|
8
|
-
-
|
|
8
|
+
- Store updates for the selected document topics will re-render this component (expected and desired for live badge updates).
|
|
9
9
|
|
|
10
10
|
## Possible bugs/risks
|
|
11
|
-
-
|
|
12
|
-
- markersList is loaded only on mount/shortId change; later store updates may not refresh the badge.
|
|
11
|
+
- Dynamic Tailwind class name interpolation (`text-${item.color}`) may be purged depending on Tailwind safelist config.
|
|
13
12
|
|
|
14
13
|
## Recommended improvements
|
|
15
|
-
- Keep
|
|
16
|
-
-
|
|
17
|
-
|
|
14
|
+
- Keep the direct Zustand selector approach (`documents[shortId]?.topics`) to preserve reactive UI behavior.
|
|
15
|
+
- If marker colors are dynamic, safelist all expected `text-*` classes or map colors to explicit class names.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { ComponentProps, FC,
|
|
4
|
-
import { Button
|
|
3
|
+
import { ComponentProps, FC, useState } from "react";
|
|
4
|
+
import { Button } from "@c-rex/ui/button";
|
|
5
5
|
import { Trash } from "lucide-react";
|
|
6
6
|
import { cn } from "@c-rex/utils";
|
|
7
7
|
import Link from "next/link";
|
|
@@ -20,17 +20,18 @@ type BookmarkProps = {
|
|
|
20
20
|
triggerVariant?: ComponentProps<typeof Button>["variant"];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const EMPTY_TOPICS: Array<{ id: string; label: string; color: string }> = [];
|
|
24
|
+
|
|
23
25
|
export const BookmarkButton: FC<BookmarkProps> = ({
|
|
24
26
|
shortId,
|
|
25
27
|
triggerVariant = "outline"
|
|
26
28
|
}) => {
|
|
27
|
-
const [
|
|
29
|
+
const [open, setOpen] = useState(false);
|
|
30
|
+
const document = useFavoritesStore((state) => state.documents[shortId]);
|
|
31
|
+
const markersList = document?.topics ?? EMPTY_TOPICS;
|
|
28
32
|
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
setMarkersList(useFavoritesStore.getState().documents[shortId]?.topics || []);
|
|
31
|
-
}, [shortId]);
|
|
32
33
|
return (
|
|
33
|
-
<Dialog>
|
|
34
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
34
35
|
<DialogTrigger asChild>
|
|
35
36
|
<Button variant={triggerVariant} size="icon" className="relative">
|
|
36
37
|
<FaRegBookmark className="text-primary" />
|
|
@@ -43,37 +44,38 @@ export const BookmarkButton: FC<BookmarkProps> = ({
|
|
|
43
44
|
</span>
|
|
44
45
|
)}
|
|
45
46
|
</Button>
|
|
46
|
-
|
|
47
47
|
</DialogTrigger>
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
{item.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
48
|
+
{open && (
|
|
49
|
+
<DialogContent>
|
|
50
|
+
<DialogHeader>
|
|
51
|
+
<DialogTitle>Bookmarks</DialogTitle>
|
|
52
|
+
<DialogDescription>
|
|
53
|
+
Manage your bookmarks here
|
|
54
|
+
</DialogDescription>
|
|
55
|
+
</DialogHeader>
|
|
56
|
+
<Table>
|
|
57
|
+
<TableBody>
|
|
58
|
+
{markersList.map((item) => (
|
|
59
|
+
<TableRow key={item.id} className="min-h-12">
|
|
60
|
+
<TableCell>
|
|
61
|
+
<FaRegBookmark className={cn("w-5", `text-${item.color}`)} />
|
|
62
|
+
</TableCell>
|
|
63
|
+
<TableCell>
|
|
64
|
+
<Link href={`/topics/${item.id}`}>
|
|
65
|
+
{item.label}
|
|
66
|
+
</Link>
|
|
67
|
+
</TableCell>
|
|
68
|
+
<TableCell>
|
|
69
|
+
<Button variant="destructive" size="icon">
|
|
70
|
+
<Trash className="w-5 hover:text-red-600 cursor-pointer" />
|
|
71
|
+
</Button>
|
|
72
|
+
</TableCell>
|
|
73
|
+
</TableRow>
|
|
74
|
+
))}
|
|
75
|
+
</TableBody>
|
|
76
|
+
</Table>
|
|
77
|
+
</DialogContent>
|
|
78
|
+
)}
|
|
77
79
|
</Dialog>
|
|
78
80
|
)
|
|
79
81
|
}
|
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
## Possible bugs/risks
|
|
12
12
|
- Async flows can hit race conditions during fast param/route changes.
|
|
13
|
-
-
|
|
13
|
+
- Residual risk: fetch errors are silently ignored (except abort), which can hide API issues.
|
|
14
14
|
|
|
15
15
|
## Recommended improvements
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
16
|
+
- Keep cancellation guards (AbortController + unmount guard) in place as currently implemented.
|
|
17
|
+
- Consider surfacing non-abort fetch failures through logging or telemetry.
|
|
@@ -19,10 +19,32 @@ export const FavoriteButton: FC<{ id: string, type: ResultTypes, label: string }
|
|
|
19
19
|
const [documentData, setDocumentData] = useState<{ id: string, label: string }>({ id, label });
|
|
20
20
|
|
|
21
21
|
useEffect(() => {
|
|
22
|
-
if (type
|
|
23
|
-
|
|
22
|
+
if (type !== RESULT_TYPES.TOPIC) {
|
|
23
|
+
setDocumentData({ id, label });
|
|
24
|
+
return;
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
|
|
27
|
+
let isCancelled = false;
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
|
|
30
|
+
const loadTopicDocumentData = async () => {
|
|
31
|
+
try {
|
|
32
|
+
const data = await getTopicDocumentData(id, controller.signal);
|
|
33
|
+
if (!isCancelled) {
|
|
34
|
+
setDocumentData(data);
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if ((error as Error).name === "AbortError") return;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
loadTopicDocumentData();
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
isCancelled = true;
|
|
45
|
+
controller.abort();
|
|
46
|
+
};
|
|
47
|
+
}, [id, label, type]);
|
|
26
48
|
|
|
27
49
|
const addFavorite = async (id: string) => {
|
|
28
50
|
if (type === RESULT_TYPES.DOCUMENT) {
|
|
@@ -45,17 +67,18 @@ export const FavoriteButton: FC<{ id: string, type: ResultTypes, label: string }
|
|
|
45
67
|
removeFavoriteTopic(documentData.id, id);
|
|
46
68
|
}
|
|
47
69
|
|
|
48
|
-
const getTopicDocumentData = async (topicId: string): Promise<
|
|
70
|
+
const getTopicDocumentData = async (topicId: string, signal?: AbortSignal): Promise<{ id: string, label: string }> => {
|
|
49
71
|
|
|
50
72
|
const response = await fetch(`/api/information-units/document-by-topic-id?shortId=${topicId}`, {
|
|
51
|
-
method: "GET"
|
|
73
|
+
method: "GET",
|
|
74
|
+
signal,
|
|
52
75
|
});
|
|
53
76
|
|
|
54
77
|
if (!response.ok) throw new Error("Failed to fetch document by topic id")
|
|
55
78
|
|
|
56
|
-
const { documentId, label } = await response.json();
|
|
79
|
+
const { documentId, label: documentLabel } = await response.json();
|
|
57
80
|
|
|
58
|
-
|
|
81
|
+
return { id: documentId, label: documentLabel };
|
|
59
82
|
}
|
|
60
83
|
|
|
61
84
|
if (isFavorite) {
|
|
@@ -60,29 +60,29 @@ export const useFavoritesStore = create<FavoritesStore>()(
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
const favoriteTopic = (documents: Record<string, { topics: Favorite[] }>, documentId: string, id: string, label: string, color: string): Record<string, { topics: Favorite[] }> => {
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return documentsCopy
|
|
63
|
+
const currentDocument = documents[documentId];
|
|
64
|
+
const topics = currentDocument?.topics || [];
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
...documents,
|
|
68
|
+
[documentId]: {
|
|
69
|
+
...(currentDocument?.label ? { label: currentDocument.label } : {}),
|
|
70
|
+
topics: [...topics, { id, label, color }],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
74
73
|
};
|
|
75
74
|
|
|
76
75
|
const unfavoriteTopic = (documents: Record<string, { topics: Favorite[] }>, documentId: string, id: string): Record<string, { topics: Favorite[] }> => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (notFound) {
|
|
82
|
-
return documentsCopy;
|
|
76
|
+
const currentDocument = documents[documentId];
|
|
77
|
+
if (!currentDocument) {
|
|
78
|
+
return documents;
|
|
83
79
|
}
|
|
84
80
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
81
|
+
return {
|
|
82
|
+
...documents,
|
|
83
|
+
[documentId]: {
|
|
84
|
+
...(currentDocument.label ? { label: currentDocument.label } : {}),
|
|
85
|
+
topics: currentDocument.topics.filter((topic) => topic.id !== id),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|