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 (
)
}
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 (
)
case "error":
return Failed to load tags!
}
}
return (
)
}
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 (
)
}
function LogOutButton() {
const logOutMutation = useLogOut()
const navigate = useNavigate()
function logOut() {
logOutMutation.mutate()
navigate({ to: "/", replace: true })
}
return (
)
}
export { BookmarkListActionBar }