import { autoUpdate, offset, useFloating } from "@floating-ui/react-dom" import type { Tag } from "@markone/core" import { createFileRoute, useNavigate, Link } from "@tanstack/react-router" import { memo, useCallback, useEffect, useId, useRef } from "react" import { fetchApi, useAuthenticatedQuery } from "~/api" import { ActionBar } from "~/app/bookmarks/-action-bar.tsx" import { useLogOut } from "~/auth.ts" import { useTags } from "~/bookmark/api.ts" import { Button } from "~/components/button" import { LoadingSpinner } from "~/components/loading-spinner" import { Message, MessageVariant } from "~/components/message.tsx" import { SideNav, SideNavItem } from "~/components/side-nav" import { useDocumentEvent } from "~/hooks/use-document-event.ts" import { useMnemonics } from "~/hooks/use-mnemonics.ts" import { BookmarkList } from "./-bookmark-list" import { ActionBarContentKind, DialogKind, WindowKind, useBookmarkPageStore } from "./-store" export const Route = createFileRoute("/bookmarks/")({ component: RouteComponent, }) function RouteComponent() { return (
) } function BookmarkListPane() { return (
<_SideNav />
) } function _SideNav() { const navigate = useNavigate() useMnemonics( { b: () => navigate({ to: "/bookmarks" }), c: () => navigate({ to: "/collections" }), }, { ignore: () => { const state = useBookmarkPageStore.getState() return state.dialog.kind !== DialogKind.None || state.actionBarContent.kind === ActionBarContentKind.SearchBar }, }, ) return ( ) } function BookmarkListContainer() { const searchParamsString = new URLSearchParams(Route.useSearch()).toString() const { data: bookmarks, status } = useAuthenticatedQuery( searchParamsString ? ["bookmarks", searchParamsString] : ["bookmarks"], async () => { const res = await fetchApi(searchParamsString ? `/bookmarks?${searchParamsString}` : "/bookmarks") return await res.json() }, ) const handleBookmarkListItemAction = useBookmarkPageStore((state) => state.handleBookmarkListItemAction) switch (status) { case "success": return ( 0 ? "-mt-2" : ""} alwaysExpandItem={false} bookmarks={bookmarks} onItemAction={handleBookmarkListItemAction} /> ) case "pending": return (

Loading

) case "error": return

error loading bookmarks

} } function BookmarkListActionBar({ className }: { className?: string }) { const activeWindow = useBookmarkPageStore((state) => state.activeWindow) const content = useBookmarkPageStore((state) => state.actionBarContent) const { refs, floatingStyles } = useFloating({ placement: "top", whileElementsMounted: autoUpdate, middleware: [offset(8)], }) return ( <> {(() => { switch (content.kind) { case ActionBarContentKind.Normal: return case ActionBarContentKind.StatusMessage: return

{content.message}

case ActionBarContentKind.SearchBar: return } })()}
{(() => { switch (activeWindow) { case WindowKind.TagFilter: return case WindowKind.AppMenu: return case WindowKind.None: return null } })()} ) } function SearchBar() { const searchInputId = useId() const inputRef = useRef(null) const setActionBarContent = useBookmarkPageStore((state) => state.setActionBarContent) const searchTimeoutRef = useRef | null>(null) const navigate = Route.useNavigate() const { q } = Route.useSearch() useDocumentEvent( "keydown", useCallback( (event) => { if (event.key === "Escape") { navigate({ search: (prevSearch: Record) => ({ ...prevSearch, q: undefined, }), }) setActionBarContent({ kind: ActionBarContentKind.Normal }) } }, [navigate, setActionBarContent], ), ) useEffect(() => { setInterval(() => { inputRef.current?.focus() }, 0) }, []) useEffect( () => () => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current) } }, [], ) function pushSearchQuery(event: React.ChangeEvent) { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current) } const q = event.currentTarget.value searchTimeoutRef.current = setTimeout(() => { navigate({ search: (prevSearch: Record) => { if (!q) { return { ...prevSearch, tags: undefined, q: undefined } } if (q.startsWith("#")) { const parts = q.split(" ") let searchTermBegin = -1 for (let i = 0; i < parts.length; ++i) { if (!parts[i].startsWith("#")) { searchTermBegin = i break } } const tags = (searchTermBegin >= 0 ? parts.slice(0, searchTermBegin) : parts) .map((tag) => tag.substring(1)) .join(",") const query = searchTermBegin >= 0 ? parts.slice(searchTermBegin).join(" ") : "" if (query) { return { ...prevSearch, tags, q: query } } return { ...prevSearch, tags, q: undefined } } return { ...prevSearch, tags: undefined, q } }, }) }, 500) } function closeSearchBar() { setActionBarContent({ kind: ActionBarContentKind.Normal }) } return (
) } function ActionButtons() { const setActiveWindow = useBookmarkPageStore((state) => state.setActiveWindow) const activeWindow = useBookmarkPageStore((state) => state.activeWindow) const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog) const setActionBarContent = useBookmarkPageStore((state) => state.setActionBarContent) useMnemonics( { a: addBookmark, s: openSearchBar, t: toggleTagWindow, }, { ignore: useCallback(() => useBookmarkPageStore.getState().dialog.kind !== DialogKind.None, []) }, ) function addBookmark() { setActiveDialog({ kind: DialogKind.AddBookmark }) } function openSearchBar() { setActionBarContent({ kind: ActionBarContentKind.SearchBar }) if (activeWindow !== WindowKind.TagFilter) { setActiveWindow(WindowKind.None) } } function toggleTagWindow() { setActiveWindow(activeWindow === WindowKind.TagFilter ? WindowKind.None : WindowKind.TagFilter) } function toggleAppMenu() { setActiveWindow(activeWindow === WindowKind.AppMenu ? WindowKind.None : WindowKind.AppMenu) } return (
) } function TagFilterWindow({ ref, style }: { ref: React.Ref; style: React.CSSProperties }) { const { data: tags, status } = useTags() const navigate = Route.useNavigate() const { tags: tagsQuery } = Route.useSearch() const filterByTags = new Set(tagsQuery ? tagsQuery.split(" ") : []) const onTagFilterItemChange = useCallback( (tag: Tag, selected: boolean) => { navigate({ search: (prevSearch: Record) => { const tagsQuery = prevSearch.tags const tags = tagsQuery ? tagsQuery.split(" ").filter((tagName) => Boolean(tagName)) : [] if (selected) { tags.push(tag.name) } else { const i = tags.indexOf(tag.name) if (i >= 0) { tags.splice(i, 1) } } if (tags.length === 0) { return { ...prevSearch, tags: undefined } } return { ...prevSearch, tags: tags.join(" ") } }, }) }, [navigate], ) function content() { switch (status) { case "pending": return (

Loading tags

) case "success": return (
    {tags.map((tag) => ( ))}
) case "error": return Failed to load tags! } } return (

TAGS

{content()}
) } const TagFilterItem = memo( ({ tag, checked, onChange }: { tag: Tag; checked: boolean; onChange: (tag: Tag, selected: boolean) => void }) => { const id = useId() return (
  • { onChange(tag, event.target.checked) }} />
  • ) }, ) function AppMenuWindow({ ref, style }: { ref: React.Ref; style: React.CSSProperties }) { return (

    MENU

    ) } function LogOutButton() { const logOutMutation = useLogOut() const navigate = useNavigate() function logOut() { logOutMutation.mutate() navigate({ to: "/", replace: true }) } return ( ) } export { BookmarkListActionBar }