Skip to content

Commit e7940ba

Browse files
committed
feat: search bookmarks
1 parent 0dfea1a commit e7940ba

17 files changed

+390
-171
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
"@types/react": "^19.1.13",
2121
"@types/react-dom": "^19.1.9",
2222
"astro": "^5.13.10",
23+
"fast-deep-equal": "^3.1.3",
2324
"react": "^19.1.1",
2425
"react-dom": "^19.1.1",
2526
"sass": "^1.93.0",
26-
"zustand": "^5.0.8"
27+
"zustand": "^5.0.8",
28+
"zustand-computed": "^2.1.0"
2729
},
2830
"packageManager": "pnpm@8.15.5+sha1.a58c038faac410c947dbdb93eb30994037d0fce2"
2931
}

pnpm-lock.yaml

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/bookmarks/Bookmark.tsx

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
faPencil,
1818
faTrash,
1919
} from "@fortawesome/free-solid-svg-icons";
20+
import { CommonBookmarkActions } from "./CommonBookmarkActions";
2021

2122
export type BookmarkPropsBase = {
2223
bookmark: BookmarkItem;
@@ -51,23 +52,15 @@ export const FolderBookmark: FC<FolderBookmarkProps> = ({
5152
createBookmark(bookmark);
5253
}
5354

54-
function handleDelete(e: MouseEvent<HTMLButtonElement>) {
55-
e.stopPropagation();
56-
deleteBookmark(bookmark);
57-
}
58-
59-
function handleEdit(e: MouseEvent<HTMLButtonElement>) {
60-
e.stopPropagation();
61-
editFolder(bookmark);
62-
}
63-
6455
return (
6556
<li className="bookmark_item">
66-
<span onClick={() => setOpen(!open)} className="bookmark_title">
67-
<FontAwesomeIcon icon={open ? faFolderOpen : faFolder} />
68-
{bookmark.title}
57+
<span onClick={() => setOpen(!open)} className="bookmark_line">
58+
<span className="title">
59+
<FontAwesomeIcon icon={open ? faFolderOpen : faFolder} />
60+
{bookmark.title}
61+
</span>
6962
<div style={{ flexGrow: "1" }} /> {/* spacer */}
70-
<div className="bookmark_actions">
63+
<div className="actions">
7164
<button className="bookmark_action" onClick={handleCreateBookmark}>
7265
<FontAwesomeIcon
7366
icon={faPlus}
@@ -84,13 +77,10 @@ export const FolderBookmark: FC<FolderBookmarkProps> = ({
8477
/>
8578
</button>
8679

87-
<button className="bookmark_action" onClick={handleDelete}>
88-
<FontAwesomeIcon icon={faTrash} />
89-
</button>
90-
91-
<button className="bookmark_action" onClick={handleEdit}>
92-
<FontAwesomeIcon icon={faPencil} />
93-
</button>
80+
<CommonBookmarkActions
81+
editFn={() => editFolder(bookmark)}
82+
deleteFn={() => deleteBookmark(bookmark)}
83+
/>
9484
</div>
9585
</span>
9686

@@ -121,35 +111,23 @@ export const UrlBookmark: FC<UrlBookmarkProps> = ({
121111
editBookmark,
122112
deleteBookmark,
123113
}) => {
124-
function handleEdit(e: MouseEvent<HTMLButtonElement>) {
125-
e.preventDefault();
126-
e.stopPropagation();
127-
editBookmark(bookmark);
128-
}
129-
130-
function handleDelete(e: MouseEvent<HTMLButtonElement>) {
131-
e.preventDefault();
132-
e.stopPropagation();
133-
deleteBookmark(bookmark);
134-
}
135-
136114
return (
137115
<a
138116
href={bookmark.url}
139117
target="_blank"
140118
rel="noopener noreferrer"
141-
className="bookmark_title"
119+
className="bookmark_line"
142120
>
143-
<FontAwesomeIcon icon={faLink} />
144-
{bookmark.title}
121+
<span className="title">
122+
<FontAwesomeIcon icon={faLink} />
123+
{bookmark.title}
124+
</span>
145125
<div style={{ flexGrow: "1" }} /> {/* spacer */}
146-
<div className="bookmark_actions">
147-
<button onClick={handleEdit}>
148-
<FontAwesomeIcon icon={faPencil} />
149-
</button>
150-
<button onClick={handleDelete}>
151-
<FontAwesomeIcon icon={faTrash} />
152-
</button>
126+
<div className="actions">
127+
<CommonBookmarkActions
128+
editFn={() => editBookmark(bookmark)}
129+
deleteFn={() => deleteBookmark(bookmark)}
130+
/>
153131
</div>
154132
</a>
155133
);

src/components/bookmarks/Bookmarks.tsx

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { useState, type FC } from "react";
1+
import { useMemo, useState, type FC } from "react";
22

33
import { useBookmarksStore } from "../../store/bookmarks";
44
import { BookmarkList } from "./BookmarkList";
5-
import { FolderCreateDialog } from "./FolderCreateDialog";
5+
import { FolderCreateDialog } from "./dialogs/FolderCreateDialog";
66

77
import "../../styles/bookmarks.scss";
8-
import { BookmarkCreateDialog } from "./BookmarkCreateDialog";
8+
import { BookmarkCreateDialog } from "./dialogs/BookmarkCreateDialog";
99
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
1010
import {
1111
faBookmark,
@@ -16,14 +16,29 @@ import type {
1616
BookmarkItem,
1717
BookmarkItemFolder,
1818
BookmarkItemUrl,
19+
BookmarkSearchResultItem,
1920
} from "../../types/bookmarks";
20-
import { BookmarkDeleteDialog } from "./BookmarkDeleteDialog";
21-
import { BookmarkEditDialog } from "./BookmarkEditDialog";
22-
import { FolderEditDialog } from "./FolderEditDialog";
21+
import { BookmarkDeleteDialog } from "./dialogs/BookmarkDeleteDialog";
22+
import { BookmarkEditDialog } from "./dialogs/BookmarkEditDialog";
23+
import { FolderEditDialog } from "./dialogs/FolderEditDialog";
24+
import { BookmarkSearchResults } from "./search/BookmarkSearchResults";
2325

2426
const Bookmarks: FC = () => {
2527
const bookmarks = useBookmarksStore((store) => store.bookmarks);
2628

29+
const searchable = useBookmarksStore((store) => store.searchIndex);
30+
const [searchTerm, setSearchTerm] = useState("");
31+
const searching = useMemo(() => searchTerm !== "", [searchTerm]);
32+
const searchResults = useMemo((): BookmarkSearchResultItem[] => {
33+
if (searchTerm === "") return searchable;
34+
else
35+
return searchable.filter(
36+
({ bookmark }) =>
37+
bookmark.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
38+
bookmark.url.toLowerCase().includes(searchTerm.toLowerCase()),
39+
);
40+
}, [searchTerm, searchable]);
41+
2742
const [folderCreateDialogOpen, setFolderCreateDialogOpen] = useState(false);
2843
const [folderCreateDialogParent, setFolderCreateDialogParent] = useState<
2944
BookmarkItemFolder | undefined
@@ -104,17 +119,31 @@ const Bookmarks: FC = () => {
104119
<div className="bookmarks">
105120
<div className="header">
106121
<h2>Bookmarks</h2>
122+
<input
123+
type="search"
124+
value={searchTerm}
125+
onChange={(e) => setSearchTerm(e.target.value)}
126+
placeholder="Search Bookmarks"
127+
/>
107128
</div>
108129

109130
<div className="content">
110-
<BookmarkList
111-
bookmarks={bookmarks}
112-
createFolder={openFolderCreateDialog}
113-
createBookmark={openBookmarkCreateDialog}
114-
deleteBookmark={openBookmarkDeleteDialog}
115-
editBookmark={openBookmarkEditDialog}
116-
editFolder={openFolderEditDialog}
117-
/>
131+
{searching ? (
132+
<BookmarkSearchResults
133+
results={searchResults}
134+
editBookmark={openBookmarkEditDialog}
135+
deleteBookmark={openBookmarkDeleteDialog}
136+
/>
137+
) : (
138+
<BookmarkList
139+
bookmarks={bookmarks}
140+
createFolder={openFolderCreateDialog}
141+
createBookmark={openBookmarkCreateDialog}
142+
deleteBookmark={openBookmarkDeleteDialog}
143+
editBookmark={openBookmarkEditDialog}
144+
editFolder={openFolderEditDialog}
145+
/>
146+
)}
118147
</div>
119148

120149
<div className="footer">
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { faTrash, faPencil } from "@fortawesome/free-solid-svg-icons";
2+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3+
import type { FC, MouseEvent } from "react";
4+
5+
export type CommonBookmarkActionsProps = {
6+
editFn: () => void;
7+
deleteFn: () => void;
8+
};
9+
10+
export const CommonBookmarkActions: FC<CommonBookmarkActionsProps> = ({
11+
editFn,
12+
deleteFn,
13+
}) => {
14+
function handleEdit(e: MouseEvent<HTMLButtonElement>) {
15+
e.preventDefault();
16+
e.stopPropagation();
17+
editFn();
18+
}
19+
20+
function handleDelete(e: MouseEvent<HTMLButtonElement>) {
21+
e.preventDefault();
22+
e.stopPropagation();
23+
deleteFn();
24+
}
25+
26+
return (
27+
<>
28+
<button className="bookmark_action" onClick={handleEdit}>
29+
<FontAwesomeIcon icon={faPencil} />
30+
</button>
31+
32+
<button className="bookmark_action" onClick={handleDelete}>
33+
<FontAwesomeIcon icon={faTrash} />
34+
</button>
35+
</>
36+
);
37+
};

src/components/bookmarks/BookmarkCreateDialog.tsx renamed to src/components/bookmarks/dialogs/BookmarkCreateDialog.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
import { useState, type FC, type FormEvent } from "react";
2-
import { Dialog, type DialogPropsBase } from "../dialog/Dialog";
3-
import { DialogContent } from "../dialog/DialogContent";
4-
import { DialogHeader } from "../dialog/DialogHeader";
5-
import { DialogActionButton } from "../dialog/DialogActionButton";
6-
import { useBookmarksStore } from "../../store/bookmarks";
7-
import { DialogFooter } from "../dialog/DialogFooter";
8-
import type { BookmarkItemFolder } from "../../types/bookmarks";
9-
import { isUrl } from "../../util/url";
10-
import { FormErrors } from "../form/FormErrors";
1+
import { useState, type FC } from "react";
2+
3+
import {
4+
Dialog,
5+
DialogFooter,
6+
DialogActionButton,
7+
DialogHeader,
8+
DialogContent,
9+
type DialogPropsBase,
10+
} from "../../dialog";
11+
import { useBookmarksStore } from "../../../store/bookmarks";
12+
import { isUrl } from "../../../util/url";
13+
import { FormErrors } from "../../form/FormErrors";
14+
import type { BookmarkItemFolder } from "../../../types/bookmarks";
1115

1216
export type BookmarkCreateDialogProps = DialogPropsBase & {
1317
onClose: () => void;

src/components/bookmarks/BookmarkDeleteDialog.tsx renamed to src/components/bookmarks/dialogs/BookmarkDeleteDialog.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import type { FC } from "react";
2-
import { Dialog, type DialogPropsBase } from "../dialog/Dialog";
3-
import { DialogHeader } from "../dialog/DialogHeader";
4-
import { BookmarkType, type BookmarkItem } from "../../types/bookmarks";
5-
import { DialogContent } from "../dialog/DialogContent";
6-
import { DialogFooter } from "../dialog/DialogFooter";
7-
import { DialogActionButton } from "../dialog/DialogActionButton";
8-
import { useBookmarksStore } from "../../store/bookmarks";
2+
import {
3+
Dialog,
4+
DialogFooter,
5+
DialogActionButton,
6+
DialogHeader,
7+
DialogContent,
8+
type DialogPropsBase,
9+
} from "../../dialog";
10+
import { BookmarkType, type BookmarkItem } from "../../../types/bookmarks";
11+
import { useBookmarksStore } from "../../../store/bookmarks";
912

1013
export type BookmarkDeleteDialogProps = DialogPropsBase & {
1114
onClose: () => void;

src/components/bookmarks/BookmarkEditDialog.tsx renamed to src/components/bookmarks/dialogs/BookmarkEditDialog.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { useState, type FC } from "react";
2-
import { useBookmarksStore } from "../../store/bookmarks";
3-
import { isUrl } from "../../util/url";
4-
import { Dialog } from "../dialog/Dialog";
5-
import { DialogContent } from "../dialog/DialogContent";
6-
import { DialogHeader } from "../dialog/DialogHeader";
7-
import { DialogFooter } from "../dialog/DialogFooter";
8-
import { DialogActionButton } from "../dialog/DialogActionButton";
9-
import { FormErrors } from "../form/FormErrors";
10-
import type { BookmarkItem, BookmarkItemUrl } from "../../types/bookmarks";
2+
import { useBookmarksStore } from "../../../store/bookmarks";
3+
import { isUrl } from "../../../util/url";
4+
import {
5+
Dialog,
6+
DialogFooter,
7+
DialogActionButton,
8+
DialogHeader,
9+
DialogContent,
10+
type DialogPropsBase,
11+
} from "../../dialog";
12+
import { FormErrors } from "../../form/FormErrors";
13+
import type { BookmarkItemUrl } from "../../../types/bookmarks";
1114

12-
export type BookmarkEditDialogProps = {
13-
open: boolean;
15+
export type BookmarkEditDialogProps = DialogPropsBase & {
1416
onClose: () => void;
1517
bookmark?: BookmarkItemUrl;
1618
};

src/components/bookmarks/FolderCreateDialog.tsx renamed to src/components/bookmarks/dialogs/FolderCreateDialog.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { useState, type FC } from "react";
2-
import { Dialog, type DialogPropsBase } from "../dialog/Dialog";
3-
import { DialogHeader } from "../dialog/DialogHeader";
4-
import { useBookmarksStore } from "../../store/bookmarks";
5-
import { DialogContent } from "../dialog/DialogContent";
6-
import { DialogFooter } from "../dialog/DialogFooter";
7-
import { DialogActionButton } from "../dialog/DialogActionButton";
8-
import type { BookmarkItemFolder } from "../../types/bookmarks";
9-
import { FormErrors } from "../form/FormErrors";
2+
import {
3+
Dialog,
4+
DialogFooter,
5+
DialogActionButton,
6+
DialogHeader,
7+
DialogContent,
8+
type DialogPropsBase,
9+
} from "../../dialog";
10+
import { useBookmarksStore } from "../../../store/bookmarks";
11+
import type { BookmarkItemFolder } from "../../../types/bookmarks";
12+
import { FormErrors } from "../../form/FormErrors";
1013

1114
export type FolderCreateDialogProps = DialogPropsBase & {
1215
onClose: () => void;

0 commit comments

Comments
 (0)