diff --git a/packages/web/src/app/bookmarks/-bookmark-list.tsx b/packages/web/src/app/bookmarks/-bookmark-list.tsx
index cd11cf0..a3234b0 100644
--- a/packages/web/src/app/bookmarks/-bookmark-list.tsx
+++ b/packages/web/src/app/bookmarks/-bookmark-list.tsx
@@ -1,112 +1,125 @@
import type { Bookmark } from "@markone/core"
import { Link } from "@tanstack/react-router"
import clsx from "clsx"
-import { createContext, memo, useCallback, useContext, useEffect, useRef } from "react"
+import { memo, useCallback } from "react"
import { twMerge } from "tailwind-merge"
-import { createStore, useStore } from "zustand"
-import { subscribeWithSelector } from "zustand/middleware"
import { useBookmarkTags } from "~/bookmark/api"
import { Button } from "~/components/button"
-import { LoadingSpinner } from "~/components/loading-spinner"
-import { useMnemonics } from "~/hooks/use-mnemonics"
+import { List } from "~/components/list"
import { DialogKind, useBookmarkPageStore } from "./-store"
-const LONG_PRESS_DELAY_MS = 500
-
-enum BookmarkListItemAction {
+export enum BookmarkListItemAction {
Open = "Open",
Edit = "Edit",
Delete = "Delete",
CopyLink = "CopyLink",
}
-type SelectionChangeCallback = (bookmark: Bookmark) => void
-type ItemActionCallback = (bookmark: Bookmark, action: BookmarkListItemAction) => void
-
interface BookmarkListProps {
bookmarks: Bookmark[]
selectedBookmarkId?: string
alwaysExpandItem: boolean
- onSelectionChange?: SelectionChangeCallback
+ onSelectionChange?: (bookmark: Bookmark) => void
onItemAction: (bookmark: Bookmark, action: BookmarkListItemAction) => void
className?: string
}
-interface CreateStoreOptions {
- bookmarks: Bookmark[]
- selectedBookmarkId?: string
- alwaysExpandItem: boolean
- onItemAction: ItemActionCallback
-}
+const BookmarkTagList = memo(function BookmarkTagList({ bookmark }: { bookmark: Bookmark }) {
+ const { data: tags } = useBookmarkTags(bookmark)
-interface BookmarkListState {
- bookmarks: Bookmark[]
- selectedIndex: number
- selectedBookmarkId: string
- alwaysExpandItem: boolean
- isItemExpanded: boolean
+ return tags ? (
+
+ {tags.map((tag) => (
+
+ {tag.name}
+
+ ))}
+
+ ) : null
+})
- onItemAction: ItemActionCallback
- setBookmarks: (bookmarks: Bookmark[]) => void
- setSelectedIndex: (index: number) => void
- setSelectedBookmarkId: (id: string) => void
- setIsItemExpanded: (expanded: boolean) => void
-}
-
-type BookmarkListStore = ReturnType
-
-const BookmarkListStoreContext = createContext(null)
-
-function createBookmarkListStore({
- bookmarks,
- selectedBookmarkId,
- alwaysExpandItem,
+const BookmarkListItem = memo(function BookmarkListItem({
+ bookmark,
+ isSelected,
+ isExpanded,
+ onSelect,
+ onExpand,
onItemAction,
-}: CreateStoreOptions) {
- let _selectedBookmarkId = selectedBookmarkId
- if (!_selectedBookmarkId && bookmarks.length > 0) {
- _selectedBookmarkId = bookmarks[0].id
- }
+ alwaysExpandItem,
+}: {
+ bookmark: Bookmark
+ isSelected: boolean
+ isExpanded: boolean
+ onSelect: () => void
+ onExpand: () => void
+ onItemAction: (bookmark: Bookmark, action: BookmarkListItemAction) => void
+ alwaysExpandItem: boolean
+}) {
+ const url = new URL(bookmark.url)
- return createStore()(
- subscribeWithSelector((set) => ({
- bookmarks,
- alwaysExpandItem,
- selectedIndex: selectedBookmarkId ? bookmarks.findIndex((bookmark) => bookmark.id === selectedBookmarkId) : 0,
- selectedBookmarkId: _selectedBookmarkId ?? "",
- isItemExpanded: alwaysExpandItem,
-
- onItemAction,
-
- setBookmarks(bookmarks: Bookmark[]) {
- set({ bookmarks, selectedBookmarkId: bookmarks.length > 0 ? bookmarks[0].id : "", selectedIndex: 0 })
- },
-
- setSelectedIndex(index: number) {
- set({ selectedIndex: index })
- },
-
- setSelectedBookmarkId(id: string) {
- set({ selectedBookmarkId: id })
- },
-
- setIsItemExpanded(expanded: boolean) {
- set({ isItemExpanded: expanded })
- },
- })),
+ return (
+
+
+
+
+ {bookmark.title}
+
+
{url.host}
+ {isExpanded ? (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ ) : null}
+
+
)
-}
-
-function useBookmarkListStoreContext() {
- const store = useContext(BookmarkListStoreContext)
- if (!store) throw new Error("BookmarkListStoreContext not found")
- return store
-}
-
-function useBookmarkListStore(selector: (state: BookmarkListState) => T): T {
- const store = useBookmarkListStoreContext()
- return useStore(store, selector)
-}
+})
function BookmarkList({
bookmarks,
@@ -116,295 +129,38 @@ function BookmarkList({
onItemAction,
className,
}: BookmarkListProps) {
- const storeRef = useRef(null)
- if (!storeRef.current) {
- storeRef.current = createBookmarkListStore({ bookmarks, selectedBookmarkId, alwaysExpandItem, onItemAction })
- }
+ const handleSelect = useCallback(
+ (bookmark: Bookmark) => {
+ onSelectionChange?.(bookmark)
+ },
+ [onSelectionChange],
+ )
- const setSelectedBookmarkId = useStore(storeRef.current, (state) => state.setSelectedBookmarkId)
-
- useEffect(() => {
- // biome-ignore lint/style/noNonNullAssertion: storeRef.current is already set above, so cant be null
- const store = storeRef.current!
- store.getState().setBookmarks(bookmarks)
- }, [bookmarks])
-
- useEffect(() => {
- // biome-ignore lint/style/noNonNullAssertion: storeRef.current is already set above, so cant be null
- const store = storeRef.current!
- const unsub = store.subscribe(
- (state) => state,
- ({ bookmarks, selectedIndex }) => {
- onSelectionChange?.(bookmarks[selectedIndex])
- },
- {
- equalityFn: (stateA, stateB) => stateA.selectedIndex === stateB.selectedIndex,
- },
- )
- return () => {
- unsub()
- }
- }, [onSelectionChange])
-
- useEffect(() => {
- // biome-ignore lint/style/noNonNullAssertion: storeRef.current is already set above, so cant be null
- const store = storeRef.current!
- if (selectedBookmarkId !== store.getState().selectedBookmarkId && selectedBookmarkId) {
- setSelectedBookmarkId(selectedBookmarkId)
- }
- }, [setSelectedBookmarkId, selectedBookmarkId])
+ const handleExpand = useCallback((bookmark: Bookmark) => {
+ // No-op since expansion is handled by the List component
+ }, [])
return (
-
- <_BookmarkList className={className} />
-
+ (
+
+ )}
+ />
)
}
-const _BookmarkList = memo(({ className }: { className?: string }) => {
- const store = useBookmarkListStoreContext()
-
- useMnemonics(
- {
- j: selectNextItem,
- ArrowDown: selectNextItem,
-
- k: selectPrevItem,
- ArrowUp: selectPrevItem,
-
- h: collapseItem,
- ArrowLeft: collapseItem,
-
- l: expandItem,
- ArrowRight: expandItem,
-
- d: deleteItem,
-
- Enter: openItem,
-
- c: (event) => {
- console.log("event", event)
- if (event.ctrlKey || event.metaKey) {
- event.preventDefault()
- copyBookmarkLink()
- }
- },
-
- e: editItem,
- },
- {
- ignore: useCallback(() => useBookmarkPageStore.getState().dialog.kind !== DialogKind.None, []),
- },
- )
-
- async function copyBookmarkLink() {
- const { bookmarks, selectedIndex, onItemAction } = store.getState()
- onItemAction(bookmarks[selectedIndex], BookmarkListItemAction.CopyLink)
- }
-
- function openItem() {
- const { bookmarks, selectedIndex, onItemAction } = store.getState()
- expandItem()
- onItemAction(bookmarks[selectedIndex], BookmarkListItemAction.Open)
- }
-
- function deleteItem() {
- const { bookmarks, selectedIndex, onItemAction } = store.getState()
- onItemAction(bookmarks[selectedIndex], BookmarkListItemAction.Delete)
- }
-
- function selectPrevItem() {
- const { bookmarks, selectedIndex, setSelectedBookmarkId } = store.getState()
- const prevIndex = selectedIndex - 1
- if (prevIndex >= 0) {
- setSelectedBookmarkId(bookmarks[prevIndex].id)
- }
- }
-
- function selectNextItem() {
- const { bookmarks, selectedIndex, setSelectedBookmarkId } = store.getState()
- const nextIndex = selectedIndex + 1
- if (nextIndex < bookmarks.length) {
- setSelectedBookmarkId(bookmarks[nextIndex].id)
- }
- }
-
- function expandItem() {
- store.getState().setIsItemExpanded(true)
- }
-
- function collapseItem() {
- const state = store.getState()
- if (!state.alwaysExpandItem) {
- store.getState().setIsItemExpanded(false)
- }
- }
-
- function editItem() {
- const { bookmarks, selectedIndex, onItemAction } = store.getState()
- onItemAction(bookmarks[selectedIndex], BookmarkListItemAction.Edit)
- }
-
- return (
-
- )
-})
-
-function ListContainer() {
- const bookmarks = useBookmarkListStore((state) => state.bookmarks)
- const selectedItemId = useBookmarkListStore((state) => state.selectedBookmarkId)
-
- return bookmarks.length === 0 ? (
- You have not saved any bookmark!
- ) : (
- bookmarks.map((bookmark, i) => (
-
- ))
- )
-}
-
-const BookmarkListItem = memo(
- ({ bookmark, index, selected }: { bookmark: Bookmark; index: number; selected: boolean }) => {
- const url = new URL(bookmark.url)
- const store = useBookmarkListStoreContext()
- const alwaysExpandItem = useBookmarkListStore((state) => state.alwaysExpandItem)
- const isBookmarkItemExpanded = useBookmarkListStore((state) => state.alwaysExpandItem || state.isItemExpanded)
- const setSelectedBookmarkId = useBookmarkListStore((state) => state.setSelectedBookmarkId)
- const setIsItemExpanded = useBookmarkListStore((state) => state.setIsItemExpanded)
- const onItemAction = useBookmarkListStore((state) => state.onItemAction)
-
- const longPressTimerRef = useRef | null>(null)
- const isLongPressActive = useRef(false)
-
- useEffect(() => {
- if (selected) {
- store.getState().setSelectedIndex(index)
- }
- }, [selected, index, store])
-
- function handleTouchStart() {
- isLongPressActive.current = true
- longPressTimerRef.current = setTimeout(() => {
- if (isLongPressActive.current) {
- setSelectedBookmarkId(bookmark.id)
- setIsItemExpanded(true)
- }
- }, LONG_PRESS_DELAY_MS)
- }
-
- function handleTouchEnd() {
- isLongPressActive.current = false
- if (longPressTimerRef.current) {
- clearTimeout(longPressTimerRef.current)
- }
- }
-
- useEffect(
- () => () => {
- if (longPressTimerRef.current) {
- clearTimeout(longPressTimerRef.current)
- }
- },
- [],
- )
-
- function deleteItem() {
- onItemAction(bookmark, BookmarkListItemAction.Delete)
- }
-
- function copyItemLink() {
- onItemAction(bookmark, BookmarkListItemAction.CopyLink)
- }
-
- function editItem() {
- onItemAction(bookmark, BookmarkListItemAction.Edit)
- }
-
- function onItemHover() {
- if (!store.getState().isItemExpanded) {
- setSelectedBookmarkId(bookmark.id)
- }
- }
-
- return (
-
-
-
-
- {bookmark.title}
-
-
{url.host}
- {isBookmarkItemExpanded && selected ? (
- <>
-
-
-
-
-
-
-
-
-
- >
- ) : null}
-
-
- )
- },
-)
-
-function BookmarkTagList({ bookmark }: { bookmark: Bookmark }) {
- const { data: tags, status } = useBookmarkTags(bookmark)
- switch (status) {
- case "pending":
- return
- case "success":
- return (
-
- {tags.map((tag) => (
-
- #{tag.name}
-
- ))}
-
- )
- case "error":
- return null
- }
-}
-
-export { BookmarkList, BookmarkListItemAction }
+export { BookmarkList }
diff --git a/packages/web/src/app/collections/-collection-list.tsx b/packages/web/src/app/collections/-collection-list.tsx
index 652e5dc..9c04fcb 100644
--- a/packages/web/src/app/collections/-collection-list.tsx
+++ b/packages/web/src/app/collections/-collection-list.tsx
@@ -1,259 +1,105 @@
-import { create } from "zustand/react"
-import { subscribeWithSelector } from "zustand/middleware"
import type { Collection } from "@markone/core"
-import { createContext, memo, useCallback, useContext, useEffect, useRef } from "react"
import { clsx } from "clsx"
+import { memo } from "react"
import { Button } from "~/components/button"
-import { useMnemonics } from "~/hooks/use-mnemonics"
-import { DialogKind, useCollectionPageStore } from "./-store"
-import { useStore } from "zustand"
+import { Link } from "~/components/link"
+import { List } from "~/components/list"
export enum CollectionListItemAction {
Delete = "Delete",
Edit = "Edit",
}
-type ItemActionCallback = (collection: Collection, action: CollectionListItemAction) => void
-
interface CollectionListProps {
collections: Collection[]
className?: string
-}
-
-interface CreateStoreOptions {
- collections: Collection[]
-}
-
-interface CollectionListState {
- collections: Collection[]
- selectedIndex: number
- selectedCollectionId: string
- isItemExpanded: boolean
-
- setCollections: (collections: Collection[]) => void
- setSelectedIndex: (index: number) => void
- setSelectedCollectionId: (id: string) => void
- setIsItemExpanded: (expanded: boolean) => void
-}
-
-type CollectionListStore = ReturnType
-
-const CollectionListStoreContext = createContext(null)
-
-function createCollectionListStore({ collections }: CreateStoreOptions) {
- return create()(
- subscribeWithSelector((set) => ({
- collections,
- selectedIndex: 0,
- selectedCollectionId: collections.length > 0 ? collections[0].id : "",
- isItemExpanded: false,
-
- setCollections(collections: Collection[]) {
- set({ collections, selectedCollectionId: collections.length > 0 ? collections[0].id : "", selectedIndex: 0 })
- },
-
- setSelectedIndex(index: number) {
- set({ selectedIndex: index })
- },
-
- setSelectedCollectionId(id: string) {
- set({ selectedCollectionId: id })
- },
-
- setIsItemExpanded(expanded: boolean) {
- set({ isItemExpanded: expanded })
- },
- })),
- )
-}
-
-function useCollectionListStoreContext() {
- const store = useContext(CollectionListStoreContext)
- if (!store) throw new Error("CollectionListStoreContext not found")
- return store
-}
-
-function useCollectionListStore(selector: (state: CollectionListState) => T): T {
- const store = useCollectionListStoreContext()
- return useStore(store, selector)
-}
-
-function CollectionList({ collections, className }: CollectionListProps) {
- const storeRef = useRef(null)
- if (!storeRef.current) {
- storeRef.current = createCollectionListStore({ collections })
- }
-
- useEffect(() => {
- // biome-ignore lint/style/noNonNullAssertion: storeRef.current is already set above, so cant be null
- const store = storeRef.current!
- store.getState().setCollections(collections)
- }, [collections])
-
- return (
-
- <_CollectionList className={className} />
-
- )
-}
-
-const _CollectionList = memo(({ className }: { className?: string }) => {
- const store = useCollectionListStoreContext()
- const handleCollectionListAction = useCollectionPageStore((state) => state.handleCollectionListAction)
-
- useMnemonics(
- {
- j: selectNextItem,
- ArrowDown: selectNextItem,
-
- k: selectPrevItem,
- ArrowUp: selectPrevItem,
-
- h: collapseItem,
- ArrowLeft: collapseItem,
-
- l: expandItem,
- ArrowRight: expandItem,
-
- d: deleteItem,
- e: editItem,
- },
- {
- ignore: useCallback(() => useCollectionPageStore.getState().dialog.kind !== DialogKind.None, []),
- },
- )
-
- function deleteItem() {
- const { collections, selectedIndex } = store.getState()
- handleCollectionListAction(collections[selectedIndex], CollectionListItemAction.Delete)
- }
-
- function editItem() {
- const { collections, selectedIndex } = store.getState()
- handleCollectionListAction(collections[selectedIndex], CollectionListItemAction.Edit)
- }
-
- function selectPrevItem() {
- const { collections, selectedIndex, setSelectedCollectionId } = store.getState()
- const prevIndex = selectedIndex - 1
- if (prevIndex >= 0) {
- setSelectedCollectionId(collections[prevIndex].id)
- }
- }
-
- function selectNextItem() {
- const { collections, selectedIndex, setSelectedCollectionId } = store.getState()
- const nextIndex = selectedIndex + 1
- if (nextIndex < collections.length) {
- setSelectedCollectionId(collections[nextIndex].id)
- }
- }
-
- function expandItem() {
- store.getState().setIsItemExpanded(true)
- }
-
- function collapseItem() {
- store.getState().setIsItemExpanded(false)
- }
-
- return (
-
- )
-})
-
-function ListContainer() {
- const collections = useCollectionListStore((state) => state.collections)
- const selectedItemId = useCollectionListStore((state) => state.selectedCollectionId)
-
- return collections.length === 0 ? (
- You have not created any collections!
- ) : (
- collections.map((collection, index) => (
-
- ))
- )
+ onItemAction: (collection: Collection, action: CollectionListItemAction) => void
}
const CollectionListItem = memo(
- ({ collection, selected, index }: { collection: Collection; selected: boolean; index: number }) => {
- const store = useCollectionListStoreContext()
- const isItemExpanded = useCollectionListStore((state) => state.isItemExpanded)
- const setSelectedCollectionId = useCollectionListStore((state) => state.setSelectedCollectionId)
- const setIsItemExpanded = useCollectionListStore((state) => state.setIsItemExpanded)
- const handleCollectionListAction = useCollectionPageStore((state) => state.handleCollectionListAction)
-
- useEffect(() => {
- if (selected) {
- store.getState().setSelectedIndex(index)
- }
- }, [selected, index, store])
-
- function onItemHover() {
- if (!store.getState().isItemExpanded) {
- setSelectedCollectionId(collection.id)
- }
- }
-
- function deleteItem() {
- handleCollectionListAction(collection, CollectionListItemAction.Delete)
- }
-
- function editItem() {
- handleCollectionListAction(collection, CollectionListItemAction.Edit)
- }
-
- return (
- void
+ onExpand: () => void
+ onItemAction: (collection: Collection, action: CollectionListItemAction) => void
+ }) => (
+
+