implement bookmark collections and enhance bookmark search functionality

This commit is contained in:
2025-06-02 17:31:36 +01:00
parent 6625451d47
commit 190c5c9caa
20 changed files with 758 additions and 427 deletions

View File

@@ -18,6 +18,7 @@ import { Route as BookmarksImport } from "./bookmarks"
import { Route as IndexImport } from "./index"
import { Route as CollectionsIndexImport } from "./collections/index"
import { Route as BookmarksIndexImport } from "./bookmarks/index"
import { Route as CollectionsCollectionIdImport } from "./collections/$collectionId"
import { Route as BookmarksBookmarkIdImport } from "./bookmarks/$bookmarkId"
// Create/Update Routes
@@ -64,6 +65,12 @@ const BookmarksIndexRoute = BookmarksIndexImport.update({
getParentRoute: () => BookmarksRoute,
} as any)
const CollectionsCollectionIdRoute = CollectionsCollectionIdImport.update({
id: "/$collectionId",
path: "/$collectionId",
getParentRoute: () => CollectionsRoute,
} as any)
const BookmarksBookmarkIdRoute = BookmarksBookmarkIdImport.update({
id: "/$bookmarkId",
path: "/$bookmarkId",
@@ -116,6 +123,13 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof BookmarksBookmarkIdImport
parentRoute: typeof BookmarksImport
}
"/collections/$collectionId": {
id: "/collections/$collectionId"
path: "/$collectionId"
fullPath: "/collections/$collectionId"
preLoaderRoute: typeof CollectionsCollectionIdImport
parentRoute: typeof CollectionsImport
}
"/bookmarks/": {
id: "/bookmarks/"
path: "/"
@@ -150,10 +164,12 @@ const BookmarksRouteWithChildren = BookmarksRoute._addFileChildren(
)
interface CollectionsRouteChildren {
CollectionsCollectionIdRoute: typeof CollectionsCollectionIdRoute
CollectionsIndexRoute: typeof CollectionsIndexRoute
}
const CollectionsRouteChildren: CollectionsRouteChildren = {
CollectionsCollectionIdRoute: CollectionsCollectionIdRoute,
CollectionsIndexRoute: CollectionsIndexRoute,
}
@@ -168,6 +184,7 @@ export interface FileRoutesByFullPath {
"/login": typeof LoginRoute
"/signup": typeof SignupRoute
"/bookmarks/$bookmarkId": typeof BookmarksBookmarkIdRoute
"/collections/$collectionId": typeof CollectionsCollectionIdRoute
"/bookmarks/": typeof BookmarksIndexRoute
"/collections/": typeof CollectionsIndexRoute
}
@@ -177,6 +194,7 @@ export interface FileRoutesByTo {
"/login": typeof LoginRoute
"/signup": typeof SignupRoute
"/bookmarks/$bookmarkId": typeof BookmarksBookmarkIdRoute
"/collections/$collectionId": typeof CollectionsCollectionIdRoute
"/bookmarks": typeof BookmarksIndexRoute
"/collections": typeof CollectionsIndexRoute
}
@@ -189,6 +207,7 @@ export interface FileRoutesById {
"/login": typeof LoginRoute
"/signup": typeof SignupRoute
"/bookmarks/$bookmarkId": typeof BookmarksBookmarkIdRoute
"/collections/$collectionId": typeof CollectionsCollectionIdRoute
"/bookmarks/": typeof BookmarksIndexRoute
"/collections/": typeof CollectionsIndexRoute
}
@@ -202,6 +221,7 @@ export interface FileRouteTypes {
| "/login"
| "/signup"
| "/bookmarks/$bookmarkId"
| "/collections/$collectionId"
| "/bookmarks/"
| "/collections/"
fileRoutesByTo: FileRoutesByTo
@@ -210,6 +230,7 @@ export interface FileRouteTypes {
| "/login"
| "/signup"
| "/bookmarks/$bookmarkId"
| "/collections/$collectionId"
| "/bookmarks"
| "/collections"
id:
@@ -220,6 +241,7 @@ export interface FileRouteTypes {
| "/login"
| "/signup"
| "/bookmarks/$bookmarkId"
| "/collections/$collectionId"
| "/bookmarks/"
| "/collections/"
fileRoutesById: FileRoutesById
@@ -271,6 +293,7 @@ export const routeTree = rootRoute
"/collections": {
"filePath": "collections.tsx",
"children": [
"/collections/$collectionId",
"/collections/"
]
},
@@ -284,6 +307,10 @@ export const routeTree = rootRoute
"filePath": "bookmarks/$bookmarkId.tsx",
"parent": "/bookmarks"
},
"/collections/$collectionId": {
"filePath": "collections/$collectionId.tsx",
"parent": "/collections"
},
"/bookmarks/": {
"filePath": "bookmarks/index.tsx",
"parent": "/bookmarks"

View File

@@ -0,0 +1,43 @@
import { Link, useRouterState, useNavigate } from "@tanstack/react-router"
import { useMnemonics } from "~/hooks/use-mnemonics"
function SideNav({ enableMnemonics }: { enableMnemonics: boolean }) {
const navigate = useNavigate()
useMnemonics(
{
b: () => navigate({ to: "/bookmarks" }),
c: () => navigate({ to: "/collections" }),
},
{ ignore: () => !enableMnemonics },
)
return (
<aside className="mb-4 md:mb-0 md:mr-24 text-start">
<nav className="mb-8">
<ul className="flex flex-col space-y-2">
<NavItem to="/bookmarks" label="BOOKMARKS" />
<NavItem to="/collections" label="COLLECTIONS" />
</ul>
</nav>
</aside>
)
}
function NavItem({ to, label }: { to: string; label: string }) {
const currentPath = useRouterState({ select: (state) => state.location.pathname })
return (
<li>
<Link to={to}>
<span className="font-bold">[{currentPath === to ? "•" : " "}]</span>{" "}
<span className={currentPath === to ? "font-bold" : ""}>
<span className="underline">{label.charAt(0)}</span>
{label.substring(1)}
</span>
</Link>
</li>
)
}
export { SideNav }

View File

@@ -1,12 +1,13 @@
import type { Bookmark } from "@markone/core"
import { Link } from "@tanstack/react-router"
import clsx from "clsx"
import { memo, useCallback } from "react"
import { memo, useCallback, useRef } from "react"
import { twMerge } from "tailwind-merge"
import { useBookmarkTags } from "~/bookmark/api"
import { Button } from "~/components/button"
import { List } from "~/components/list"
import { DialogKind, useBookmarkPageStore } from "./-store"
import { List, type ListRef } from "~/components/list"
import { DialogKind, ActionBarContentKind, useBookmarkPageStore } from "./-store"
import { useMnemonics } from "~/hooks/use-mnemonics"
export enum BookmarkListItemAction {
Open = "Open",
@@ -129,6 +130,7 @@ function BookmarkList({
onItemAction,
className,
}: BookmarkListProps) {
const listRef = useRef<ListRef<Bookmark>>(null)
const handleSelect = useCallback(
(bookmark: Bookmark) => {
onSelectionChange?.(bookmark)
@@ -136,12 +138,32 @@ function BookmarkList({
[onSelectionChange],
)
const handleExpand = useCallback((bookmark: Bookmark) => {
// No-op since expansion is handled by the List component
}, [])
useMnemonics(
{
e: () => {
const selectedBookmark = listRef.current?.selectedItem
if (selectedBookmark) {
onItemAction(selectedBookmark, BookmarkListItemAction.Edit)
}
},
d: () => {
const selectedBookmark = listRef.current?.selectedItem
if (selectedBookmark) {
onItemAction(selectedBookmark, BookmarkListItemAction.Delete)
}
},
},
{
ignore: useCallback(() => {
const state = useBookmarkPageStore.getState()
return state.dialog.kind !== DialogKind.None || state.actionBarContent.kind === ActionBarContentKind.SearchBar
}, []),
},
)
return (
<List
ref={listRef}
items={bookmarks}
selectedItemId={selectedBookmarkId}
alwaysExpandItem={alwaysExpandItem}

View File

@@ -1,6 +1,6 @@
import { useEffect, useId, useRef, useState } from "react"
import { ApiErrorCode, BadRequestError } from "~/api"
import { useCreateBookmark } from "~/bookmark/api"
import { useCreateBookmark, useTags } from "~/bookmark/api"
import { Button } from "~/components/button"
import { Dialog, DialogActionRow, DialogBody, DialogTitle } from "~/components/dialog"
import { FormField } from "~/components/form-field"
@@ -13,6 +13,7 @@ import { DialogKind, useBookmarkPageStore } from "../-store"
function AddBookmarkDialog() {
const [isWebsiteUnreachable, setIsWebsiteUnreachable] = useState(false)
const createBookmarkMutation = useCreateBookmark()
const { data: tags, status: tagsStatus } = useTags()
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
const formId = useId()
const linkInputRef = useRef<HTMLInputElement | null>(null)
@@ -21,7 +22,7 @@ function AddBookmarkDialog() {
useMnemonics(
{
c: () => {
if (linkInputRef.current !== document.activeElement && tagsInputRef.current !== document.activeElement) {
if (linkInputRef.current !== document.activeElement && tagsInputRef.current?.input !== document.activeElement) {
cancel()
}
},
@@ -112,7 +113,13 @@ function AddBookmarkDialog() {
className="w-full"
labelClassName="bg-stone-300 dark:bg-stone-800"
/>
<TagsInput ref={tagsInputRef} />
{tagsStatus === "success" ? (
<TagsInput ref={tagsInputRef} tags={tags} />
) : (
<p>
Loading tags <LoadingSpinner />
</p>
)}
</form>
</DialogBody>
<DialogActionRow>

View File

@@ -1,78 +1,151 @@
import type { Bookmark, Tag } from "@markone/core"
import { useEffect, useId, useImperativeHandle, useRef } from "react"
import { useBookmarkTags, useUpdateBookmark } from "~/bookmark/api"
import type { Bookmark } from "@markone/core"
import { useEffect, useId, useRef, useState } from "react"
import { ApiErrorCode, BadRequestError } from "~/api"
import { useBookmarkTags, useTags, useUpdateBookmark } from "~/bookmark/api"
import { Button } from "~/components/button"
import { Dialog, DialogActionRow, DialogBody, DialogTitle } from "~/components/dialog"
import { FormField } from "~/components/form-field"
import { LoadingSpinner } from "~/components/loading-spinner"
import { Message, MessageVariant } from "~/components/message.tsx"
import { TagsInput, type TagsInputRef } from "~/components/tags-input"
import { useMnemonics } from "~/hooks/use-mnemonics.ts"
import { useBookmarkPageStore } from "../-store"
interface EditFormRef {
form: HTMLFormElement | null
titleInput: HTMLInputElement | null
tagsInput: TagsInputRef | null
}
import { Message, MessageVariant } from "~/components/message"
import { TagsInput, type TagsInputRef } from "~/components/tags-input.tsx"
import { useMnemonics } from "~/hooks/use-mnemonics"
import { DialogKind, useBookmarkPageStore } from "../-store"
function EditBookmarkDialog({ bookmark }: { bookmark: Bookmark }) {
const closeDialog = useBookmarkPageStore((state) => state.closeDialog)
const { data: tags, status } = useBookmarkTags(bookmark)
const editFormId = useId()
const editFormRef = useRef<EditFormRef | null>(null)
const [isWebsiteUnreachable, setIsWebsiteUnreachable] = useState(false)
const updateBookmarkMutation = useUpdateBookmark(bookmark)
const { data: tags, status: tagsStatus } = useTags()
const { data: bookmarkTags, status: bookmarkTagsStatus } = useBookmarkTags(bookmark)
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
const formId = useId()
const linkInputRef = useRef<HTMLInputElement | null>(null)
const tagsInputRef = useRef<TagsInputRef | null>(null)
useMnemonics(
{
s: () => {
if (
editFormRef.current &&
editFormRef.current.titleInput !== document.activeElement &&
editFormRef.current.tagsInput?.input !== document.activeElement
) {
editFormRef.current.form?.requestSubmit()
}
},
c: () => {
if (
editFormRef.current?.titleInput !== document.activeElement &&
editFormRef.current?.tagsInput?.input !== document.activeElement
) {
closeDialog()
if (linkInputRef.current !== document.activeElement && tagsInputRef.current?.input !== document.activeElement) {
cancel()
}
},
Escape: () => {
editFormRef.current?.titleInput?.blur()
editFormRef.current?.tagsInput?.input?.blur()
linkInputRef.current?.blur()
tagsInputRef.current?.input?.blur()
},
},
{ ignore: () => false },
)
function content() {
switch (status) {
case "pending":
return (
<p>
Loading <LoadingSpinner />
</p>
)
case "success":
return <EditForm ref={editFormRef} formId={editFormId} bookmark={bookmark} tags={tags} />
case "error":
return null
// when using autoFocus, it also captures the "a" mnemonic
// which appends the "a" to the input
// this is to prevent the "a" mnemonic to be captured as input to the text box
useEffect(() => {
setTimeout(() => {
if (linkInputRef.current) {
linkInputRef.current.focus()
}
}, 0)
}, [])
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
if (tagsInputRef.current) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const title = formData.get("title")
if (title && typeof title === "string") {
try {
await updateBookmarkMutation.mutateAsync({
title,
tags: tagsInputRef.current.tags,
})
setActiveDialog({ kind: DialogKind.None })
} catch (error) {
if (error instanceof BadRequestError && error.code === ApiErrorCode.LinkUnreachable) {
setIsWebsiteUnreachable(true)
} else {
setIsWebsiteUnreachable(false)
}
}
}
}
}
function cancel() {
setActiveDialog({ kind: DialogKind.None })
}
function message() {
if (updateBookmarkMutation.isPending) {
return (
<p>
Loading <LoadingSpinner />
</p>
)
}
if (isWebsiteUnreachable) {
return (
<Message variant={MessageVariant.Warning} className="px-4">
The link does not seem to be reachable. Click "SAVE" to save anyways.
</Message>
)
}
if (updateBookmarkMutation.status === "error") {
return (
<Message variant={MessageVariant.Error} className="px-4">
An error occurred when saving bookmark
</Message>
)
}
return null
}
function content() {
if (tagsStatus === "pending" || bookmarkTagsStatus === "pending") {
return (
<p>
Loading <LoadingSpinner />
</p>
)
}
if (tagsStatus === "error" || bookmarkTagsStatus === "error") {
return (
<Message variant={MessageVariant.Error} className="px-4">
An error occurred when loading tags
</Message>
)
}
if (tags && bookmarkTags) {
return (
<form id={formId} className="px-8" onSubmit={onSubmit}>
<FormField
ref={linkInputRef}
type="text"
name="title"
label="TITLE"
className="w-full"
labelClassName="bg-stone-300 dark:bg-stone-800"
defaultValue={bookmark.title}
/>
<TagsInput ref={tagsInputRef} tags={tags} initialSelections={bookmarkTags} />
</form>
)
}
return null
}
return (
<Dialog>
<DialogTitle>EDIT BOOKMARK</DialogTitle>
<DialogBody>{content()}</DialogBody>
<DialogBody>
{message()}
{content()}
</DialogBody>
<DialogActionRow>
<Button type="submit" form={editFormId} disabled={status !== "success"}>
<span className="underline">S</span>AVE
<Button type="submit" disabled={updateBookmarkMutation.isPending} form={formId}>
SAVE
</Button>
<Button disabled={status !== "success"} onClick={closeDialog}>
<Button type="button" disabled={updateBookmarkMutation.isPending} onClick={cancel}>
<span className="underline">C</span>ANCEL
</Button>
</DialogActionRow>
@@ -80,83 +153,4 @@ function EditBookmarkDialog({ bookmark }: { bookmark: Bookmark }) {
)
}
function EditForm({
ref,
formId,
bookmark,
tags,
}: { ref: React.Ref<EditFormRef>; formId: string; bookmark: Bookmark; tags: Tag[] }) {
const formRef = useRef<HTMLFormElement | null>(null)
const titleInputRef = useRef<HTMLInputElement | null>(null)
const tagsInputRef = useRef<TagsInputRef>(null)
const updateBookmarkMutation = useUpdateBookmark(bookmark)
const closeDialog = useBookmarkPageStore((state) => state.closeDialog)
useImperativeHandle(ref, () => ({
form: formRef.current,
titleInput: titleInputRef.current,
tagsInput: tagsInputRef.current,
}))
// when using autoFocus, it also captures the "e" mnemonic
// which appends the "e" to the input
// this is to prevent the "e" mnemonic to be captured as input to the text box
useEffect(() => {
setTimeout(() => {
titleInputRef.current?.focus()
}, 0)
}, [])
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
if (tagsInputRef.current) {
event.preventDefault()
const form = new FormData(event.currentTarget)
const title = form.get("title")
const tags = tagsInputRef.current.tags
if (title && typeof title === "string") {
try {
await updateBookmarkMutation.mutateAsync({
title,
tags,
})
closeDialog()
} catch {}
}
}
}
function message() {
switch (updateBookmarkMutation.status) {
case "pending":
return (
<p>
Saving changes <LoadingSpinner />
</p>
)
case "error":
return <Message variant={MessageVariant.Error}>Error updating the bookmark!</Message>
default:
return null
}
}
return (
<>
{message()}
<form ref={formRef} id={formId} onSubmit={onSubmit}>
<FormField
ref={titleInputRef}
type="text"
name="title"
label="TITLE"
className="w-full"
defaultValue={bookmark.title}
labelClassName="bg-stone-300 dark:bg-stone-800"
/>
<TagsInput ref={tagsInputRef} initialValue={tags.map((tag) => tag.name).join(" ")} />
</form>
</>
)
}
export { EditBookmarkDialog }

View File

@@ -59,7 +59,6 @@ interface BookmarkPageState {
bookmarkToBeEdited: Bookmark | null
layoutMode: LayoutMode
dialog: DialogData
hasDialog: boolean
actionBarContent: ActionBarContent
statusMessage: string
activeWindow: WindowKind
@@ -82,10 +81,6 @@ const useBookmarkPageStore = create<BookmarkPageState>()((set, get) => ({
statusMessage: "",
activeWindow: WindowKind.None,
get hasDialog(): boolean {
return get().dialog.kind !== DialogKind.None
},
setActiveWindow(window: WindowKind) {
set({ activeWindow: window })
},

View File

@@ -1,18 +1,19 @@
import { autoUpdate, offset, useFloating } from "@floating-ui/react-dom"
import type { Tag } from "@markone/core"
import { createFileRoute, useNavigate } from "@tanstack/react-router"
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, LinkButton } from "~/components/button"
import { Button } from "~/components/button"
import { LoadingSpinner } from "~/components/loading-spinner"
import { Message, MessageVariant } from "~/components/message.tsx"
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"
import { SideNav } from "~/app/-side-nav"
export const Route = createFileRoute("/bookmarks/")({
component: RouteComponent,
@@ -30,12 +31,7 @@ function RouteComponent() {
function BookmarkListPane() {
return (
<div className="flex flex-col py-16 container max-w-3xl md:flex-row lg:py-32">
<header className="mb-4 md:mb-0 md:mr-16 text-start">
<h1 className="font-bold text-start">
<span className="invisible md:hidden">&nbsp;&gt;&nbsp;</span>
YOUR BOOKMARKS
</h1>
</header>
<_SideNav />
<div className="flex-1">
<BookmarkListContainer />
</div>
@@ -43,6 +39,11 @@ function BookmarkListPane() {
)
}
function _SideNav() {
const hasDialog = useBookmarkPageStore((state) => state.dialog.kind !== DialogKind.None)
return <SideNav enableMnemonics={!hasDialog} />
}
function BookmarkListContainer() {
const searchParamsString = new URLSearchParams(Route.useSearch()).toString()
const { data: bookmarks, status } = useAuthenticatedQuery(
@@ -360,11 +361,6 @@ function AppMenuWindow({ ref, style }: { ref: React.Ref<HTMLDivElement>; style:
<p className="bg-stone-900 dark:bg-stone-200 text-stone-300 dark:text-stone-800 text-center">MENU</p>
<div className="p-4">
<ul className="space-x-4 flex justify-center">
<li>
<LinkButton to="/collections">
<span className="underline">C</span>OLLECTIONS
</LinkButton>
</li>
<li>
<LogOutButton />
</li>

View File

@@ -0,0 +1,185 @@
import { autoUpdate, offset, useFloating } from "@floating-ui/react-dom"
import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { useCallback } from "react"
import { ActionBar } from "~/app/bookmarks/-action-bar"
import { useLogOut } from "~/auth"
import { useCollection, useCollectionBookmarks } from "~/collection/api"
import { Button, LinkButton } from "~/components/button"
import { LoadingSpinner } from "~/components/loading-spinner"
import { useMnemonics } from "~/hooks/use-mnemonics"
import { BookmarkList } from "~/app/bookmarks/-bookmark-list"
import { DialogKind, WindowKind, useBookmarkPageStore } from "~/app/bookmarks/-store"
import type { Collection } from "@markone/core"
export const Route = createFileRoute("/collections/$collectionId")({
component: CollectionDetailPage,
})
function CollectionDetailPage() {
return (
<main className="w-full flex justify-center">
<CollectionDetailPane />
<CollectionDetailActionBar className="fixed left-0 right-0 bottom-0" />
</main>
)
}
function CollectionDetailPane() {
const { collectionId } = Route.useParams()
const { data: collection, status } = useCollection(collectionId)
if (status === "pending") {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<p>
Loading <LoadingSpinner />
</p>
</div>
)
}
if (status === "error" || !collection) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<p>Error loading collection</p>
</div>
)
}
return (
<div className="flex flex-col py-16 container max-w-3xl md:flex-row lg:py-32">
<header className="mb-4 md:mb-0 md:mr-16 text-start">
<h1 className="font-bold text-start">
<span className="invisible md:hidden">&nbsp;&gt;&nbsp;</span>
{collection.name}
</h1>
<p className="opacity-80 text-sm">{collection.description}</p>
</header>
<div className="flex-1">
<CollectionBookmarkList collection={collection} />
</div>
</div>
)
}
function CollectionBookmarkList({ collection }: { collection: Collection }) {
const { data: bookmarks, status } = useCollectionBookmarks(collection)
const handleBookmarkListItemAction = useBookmarkPageStore((state) => state.handleBookmarkListItemAction)
switch (status) {
case "success":
return (
<BookmarkList
className={bookmarks.length > 0 ? "-mt-2" : ""}
alwaysExpandItem={false}
bookmarks={bookmarks}
onItemAction={handleBookmarkListItemAction}
/>
)
case "pending":
return (
<div className="flex flex-col items-center justify-center min-h-[50vh]">
<p>
Loading <LoadingSpinner />
</p>
</div>
)
case "error":
return (
<div className="flex flex-col items-center justify-center min-h-[50vh]">
<p>Error loading bookmarks</p>
</div>
)
}
}
function CollectionDetailActionBar({ className }: { className?: string }) {
const activeWindow = useBookmarkPageStore((state) => state.activeWindow)
const { refs, floatingStyles } = useFloating({
placement: "top",
whileElementsMounted: autoUpdate,
middleware: [offset(8)],
})
return (
<>
<ActionBar ref={refs.setReference} className={className}>
<ActionButtons />
</ActionBar>
{activeWindow === WindowKind.AppMenu && <AppMenuWindow ref={refs.setFloating} style={floatingStyles} />}
</>
)
}
function ActionButtons() {
const setActiveWindow = useBookmarkPageStore((state) => state.setActiveWindow)
const activeWindow = useBookmarkPageStore((state) => state.activeWindow)
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
useMnemonics(
{
a: addBookmark,
},
{ ignore: useCallback(() => useBookmarkPageStore.getState().dialog.kind !== DialogKind.None, []) },
)
function addBookmark() {
setActiveDialog({ kind: DialogKind.AddBookmark })
}
function toggleAppMenu() {
setActiveWindow(activeWindow === WindowKind.AppMenu ? WindowKind.None : WindowKind.AppMenu)
}
return (
<div className="flex flex-row justify-center space-x-4">
<Button onClick={addBookmark}>
<span className="underline">A</span>DD BOOKMARK
</Button>
<Button onClick={toggleAppMenu}></Button>
</div>
)
}
function AppMenuWindow({ ref, style }: { ref: React.Ref<HTMLDivElement>; style: React.CSSProperties }) {
return (
<div ref={ref} style={style} className="border w-full md:w-100">
<p className="bg-stone-900 dark:bg-stone-200 text-stone-300 dark:text-stone-800 text-center">MENU</p>
<div className="p-4">
<ul className="space-x-4 space-y-2 flex flex-wrap justify-center">
<li>
<LinkButton to="/bookmarks">
<span className="underline">B</span>OOKMARKS
</LinkButton>
</li>
<li>
<LinkButton to="/collections">
<span className="underline">C</span>OLLECTIONS
</LinkButton>
</li>
<li>
<LogOutButton />
</li>
</ul>
</div>
</div>
)
}
function LogOutButton() {
const logOutMutation = useLogOut()
const navigate = useNavigate()
function logOut() {
logOutMutation.mutate()
navigate({ to: "/", replace: true })
}
return (
<Button disabled={logOutMutation.isPending} onClick={logOut}>
LOG OUT
</Button>
)
}

View File

@@ -1,8 +1,8 @@
import type { Collection } from "@markone/core"
import { Link } from "@tanstack/react-router"
import { clsx } from "clsx"
import { memo } from "react"
import { Button } from "~/components/button"
import { Link } from "~/components/link"
import { List } from "~/components/list"
export enum CollectionListItemAction {
@@ -53,7 +53,9 @@ const CollectionListItem = memo(
</button>
<div className="flex flex-col w-full">
<div className="block w-full text-start font-bold">
<Link href="#">{collection.name}</Link>
<Link to={`/collections/${collection.id}`} className={isSelected ? "underline" : ""}>
{collection.name}
</Link>
</div>
<p className="opacity-80 text-sm">{collection.description}</p>
{isExpanded ? (

View File

@@ -1,6 +1,6 @@
import { useId, useRef, useEffect } from "react"
import type { Collection } from "@markone/core"
import { useUpdateCollection } from "~/collections/api"
import { useUpdateCollection } from "~/collection/api"
import { Button } from "~/components/button"
import { Dialog, DialogActionRow, DialogBody, DialogTitle } from "~/components/dialog"
import { FormField } from "~/components/form-field"

View File

@@ -11,6 +11,7 @@ import { CollectionList } from "./-collection-list"
import { AddCollectionDialog } from "./-dialogs/add-collection-dialog"
import { DeleteCollectionDialog } from "./-dialogs/delete-collection-dialog"
import { DialogKind, WindowKind, useCollectionPageStore } from "./-store"
import { SideNav } from "~/app/-side-nav"
export const Route = createFileRoute("/collections/")({
component: CollectionsPage,
@@ -42,12 +43,7 @@ function CollectionsPage() {
function CollectionsPane() {
return (
<div className="flex flex-col py-16 container max-w-3xl md:flex-row lg:py-32">
<header className="mb-4 md:mb-0 md:mr-16 text-start">
<h1 className="font-bold text-start">
<span className="invisible md:hidden">&nbsp;&gt;&nbsp;</span>
YOUR COLLECTIONS
</h1>
</header>
<_SideNav />
<div className="flex-1">
<CollectionsContainer />
</div>
@@ -55,6 +51,11 @@ function CollectionsPane() {
)
}
function _SideNav() {
const hasDialog = useCollectionPageStore((state) => state.dialog.kind !== DialogKind.None)
return <SideNav enableMnemonics={!hasDialog} />
}
function CollectionsContainer() {
const { data: collections, status } = useCollections()
const handleCollectionListAction = useCollectionPageStore((state) => state.handleCollectionListAction)