From c73082b9c952abee48c22c989ac0dbabca0d0dda Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 13 May 2025 18:34:08 +0100 Subject: [PATCH] move bookmark preview to its own route --- packages/web/src/app/-route-tree.gen.ts | 90 ++- packages/web/src/app/bookmarks.tsx | 668 +++--------------- .../web/src/app/bookmarks/$bookmarkId.tsx | 126 ++++ .../web/src/app/bookmarks/-action-bar.tsx | 57 ++ .../web/src/app/bookmarks/-bookmark-list.tsx | 376 ++++++++++ packages/web/src/app/bookmarks/-store.tsx | 63 ++ packages/web/src/app/bookmarks/index.tsx | 68 ++ packages/web/src/hooks/use-mnemonics.ts | 3 +- packages/web/src/main.tsx | 8 +- packages/web/src/router.ts | 8 + 10 files changed, 861 insertions(+), 606 deletions(-) create mode 100644 packages/web/src/app/bookmarks/$bookmarkId.tsx create mode 100644 packages/web/src/app/bookmarks/-action-bar.tsx create mode 100644 packages/web/src/app/bookmarks/-bookmark-list.tsx create mode 100644 packages/web/src/app/bookmarks/-store.tsx create mode 100644 packages/web/src/app/bookmarks/index.tsx create mode 100644 packages/web/src/router.ts diff --git a/packages/web/src/app/-route-tree.gen.ts b/packages/web/src/app/-route-tree.gen.ts index 5528b34..cbfa424 100644 --- a/packages/web/src/app/-route-tree.gen.ts +++ b/packages/web/src/app/-route-tree.gen.ts @@ -15,6 +15,8 @@ import { Route as SignupImport } from "./signup" import { Route as LoginImport } from "./login" import { Route as BookmarksImport } from "./bookmarks" import { Route as IndexImport } from "./index" +import { Route as BookmarksIndexImport } from "./bookmarks/index" +import { Route as BookmarksBookmarkIdImport } from "./bookmarks/$bookmarkId" // Create/Update Routes @@ -42,6 +44,18 @@ const IndexRoute = IndexImport.update({ getParentRoute: () => rootRoute, } as any) +const BookmarksIndexRoute = BookmarksIndexImport.update({ + id: "/", + path: "/", + getParentRoute: () => BookmarksRoute, +} as any) + +const BookmarksBookmarkIdRoute = BookmarksBookmarkIdImport.update({ + id: "/$bookmarkId", + path: "/$bookmarkId", + getParentRoute: () => BookmarksRoute, +} as any) + // Populate the FileRoutesByPath interface declare module "@tanstack/react-router" { @@ -74,52 +88,98 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof SignupImport parentRoute: typeof rootRoute } + "/bookmarks/$bookmarkId": { + id: "/bookmarks/$bookmarkId" + path: "/$bookmarkId" + fullPath: "/bookmarks/$bookmarkId" + preLoaderRoute: typeof BookmarksBookmarkIdImport + parentRoute: typeof BookmarksImport + } + "/bookmarks/": { + id: "/bookmarks/" + path: "/" + fullPath: "/bookmarks/" + preLoaderRoute: typeof BookmarksIndexImport + parentRoute: typeof BookmarksImport + } } } // Create and export the route tree +interface BookmarksRouteChildren { + BookmarksBookmarkIdRoute: typeof BookmarksBookmarkIdRoute + BookmarksIndexRoute: typeof BookmarksIndexRoute +} + +const BookmarksRouteChildren: BookmarksRouteChildren = { + BookmarksBookmarkIdRoute: BookmarksBookmarkIdRoute, + BookmarksIndexRoute: BookmarksIndexRoute, +} + +const BookmarksRouteWithChildren = BookmarksRoute._addFileChildren( + BookmarksRouteChildren, +) + export interface FileRoutesByFullPath { "/": typeof IndexRoute - "/bookmarks": typeof BookmarksRoute + "/bookmarks": typeof BookmarksRouteWithChildren "/login": typeof LoginRoute "/signup": typeof SignupRoute + "/bookmarks/$bookmarkId": typeof BookmarksBookmarkIdRoute + "/bookmarks/": typeof BookmarksIndexRoute } export interface FileRoutesByTo { "/": typeof IndexRoute - "/bookmarks": typeof BookmarksRoute "/login": typeof LoginRoute "/signup": typeof SignupRoute + "/bookmarks/$bookmarkId": typeof BookmarksBookmarkIdRoute + "/bookmarks": typeof BookmarksIndexRoute } export interface FileRoutesById { __root__: typeof rootRoute "/": typeof IndexRoute - "/bookmarks": typeof BookmarksRoute + "/bookmarks": typeof BookmarksRouteWithChildren "/login": typeof LoginRoute "/signup": typeof SignupRoute + "/bookmarks/$bookmarkId": typeof BookmarksBookmarkIdRoute + "/bookmarks/": typeof BookmarksIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: "/" | "/bookmarks" | "/login" | "/signup" + fullPaths: + | "/" + | "/bookmarks" + | "/login" + | "/signup" + | "/bookmarks/$bookmarkId" + | "/bookmarks/" fileRoutesByTo: FileRoutesByTo - to: "/" | "/bookmarks" | "/login" | "/signup" - id: "__root__" | "/" | "/bookmarks" | "/login" | "/signup" + to: "/" | "/login" | "/signup" | "/bookmarks/$bookmarkId" | "/bookmarks" + id: + | "__root__" + | "/" + | "/bookmarks" + | "/login" + | "/signup" + | "/bookmarks/$bookmarkId" + | "/bookmarks/" fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute - BookmarksRoute: typeof BookmarksRoute + BookmarksRoute: typeof BookmarksRouteWithChildren LoginRoute: typeof LoginRoute SignupRoute: typeof SignupRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, - BookmarksRoute: BookmarksRoute, + BookmarksRoute: BookmarksRouteWithChildren, LoginRoute: LoginRoute, SignupRoute: SignupRoute, } @@ -144,13 +204,25 @@ export const routeTree = rootRoute "filePath": "index.tsx" }, "/bookmarks": { - "filePath": "bookmarks.tsx" + "filePath": "bookmarks.tsx", + "children": [ + "/bookmarks/$bookmarkId", + "/bookmarks/" + ] }, "/login": { "filePath": "login.tsx" }, "/signup": { "filePath": "signup.tsx" + }, + "/bookmarks/$bookmarkId": { + "filePath": "bookmarks/$bookmarkId.tsx", + "parent": "/bookmarks" + }, + "/bookmarks/": { + "filePath": "bookmarks/index.tsx", + "parent": "/bookmarks" } } } diff --git a/packages/web/src/app/bookmarks.tsx b/packages/web/src/app/bookmarks.tsx index ce036fe..fb72f2a 100644 --- a/packages/web/src/app/bookmarks.tsx +++ b/packages/web/src/app/bookmarks.tsx @@ -1,124 +1,29 @@ -import type { LinkBookmark } from "@markone/core/bookmark" -import { createFileRoute, useNavigate } from "@tanstack/react-router" -import clsx from "clsx" -import { useCallback, useEffect, useId, useRef, useState } from "react" -import { create } from "zustand" -import { fetchApi, useAuthenticatedQuery, BadRequestError, ApiErrorCode } from "~/api" +import { Outlet, createFileRoute } from "@tanstack/react-router" +import { useState, useId, useRef, useEffect } from "react" +import { BadRequestError, ApiErrorCode } from "~/api" import { useCreateBookmark, useDeleteBookmark } from "~/bookmark/api" import { Button } from "~/components/button" -import { Dialog, DialogActionRow, DialogBody, DialogTitle } from "~/components/dialog" -import { Message, MessageVariant } from "~/components/message" +import { Dialog, DialogTitle, DialogBody, DialogActionRow } from "~/components/dialog" import { FormField } from "~/components/form-field" import { LoadingSpinner } from "~/components/loading-spinner" +import { Message, MessageVariant } from "~/components/message" import { useMnemonics } from "~/hooks/use-mnemonics" -import { useLogOut } from "~/auth" +import { useBookmarkPageStore, ActiveDialog, LayoutMode } from "./bookmarks/-store" -const LAYOUT_MODE = { - popup: "popup", - sideBySide: "side-by-side", -} as const -type LayoutMode = (typeof LAYOUT_MODE)[keyof typeof LAYOUT_MODE] +export const Route = createFileRoute("/bookmarks")({ + component: RouteComponent, +}) -enum ActiveDialog { - None = "None", - AddBookmark = "AddBookmark", - DeleteBookmark = "DeleteBookmark", -} - -interface BookmarkPageState { - bookmarkCount: number - selectedBookmarkId: string - selectedBookmarkIndex: number - isBookmarkItemExpanded: boolean - isBookmarkPreviewOpened: boolean - bookmarkToBeDeleted: LinkBookmark | null - layoutMode: LayoutMode - activeDialog: ActiveDialog - actionBarHeight: number - - setActionBarHeight: (height: number) => void - setActiveDialog: (dialog: ActiveDialog) => void - setBookmarkItemExpanded: (isExpanded: boolean) => void - setBookmarkPreviewOpened: (isOpened: boolean) => void - setLayoutMode: (mode: LayoutMode) => void - selectBookmark: (bookmark: LinkBookmark, index: number) => void - reconcileSelection: (bookmarks: LinkBookmark[]) => void - markBookmarkForDeletion: (bookmark: LinkBookmark | null) => void -} - -const useBookmarkPageStore = create()((set, get) => ({ - bookmarkCount: 0, - bookmarks: [], - selectedBookmarkId: "", - selectedBookmarkIndex: 0, - isBookmarkItemExpanded: false, - isBookmarkPreviewOpened: false, - bookmarkToBeDeleted: null, - layoutMode: LAYOUT_MODE.popup, - activeDialog: ActiveDialog.None, - actionBarHeight: 0, - - setActionBarHeight(height: number) { - set({ actionBarHeight: height }) - }, - - setActiveDialog(dialog: ActiveDialog) { - set({ activeDialog: dialog }) - }, - - setBookmarkItemExpanded(isExpanded: boolean) { - set({ isBookmarkItemExpanded: isExpanded }) - }, - - setBookmarkPreviewOpened(isOpened: boolean) { - set({ isBookmarkPreviewOpened: isOpened }) - }, - - setLayoutMode(mode: LayoutMode) { - set({ layoutMode: mode }) - }, - - selectBookmark(bookmark: LinkBookmark, index: number) { - set({ selectedBookmarkId: bookmark.id, selectedBookmarkIndex: index }) - }, - - reconcileSelection(bookmarks: LinkBookmark[]) { - const { selectedBookmarkId, selectedBookmarkIndex } = get() - - if (bookmarks.length === 0) { - set({ selectedBookmarkId: "", selectedBookmarkIndex: 0 }) - } else if (!selectedBookmarkId) { - set({ selectedBookmarkId: bookmarks[selectedBookmarkIndex].id }) - } else { - const newIndex = bookmarks.findIndex((bookmark) => bookmark.id === selectedBookmarkId) - if (newIndex !== selectedBookmarkIndex) { - if (newIndex >= 0) { - set({ selectedBookmarkIndex: newIndex }) - } else if (selectedBookmarkIndex >= bookmarks.length - 1) { - set({ selectedBookmarkIndex: bookmarks.length - 1 }) - } else if (selectedBookmarkIndex === 0) { - set({ selectedBookmarkIndex: 0 }) - } else { - set({ selectedBookmarkIndex: selectedBookmarkIndex + 1 }) - } - } - } - }, - - markBookmarkForDeletion(bookmark: LinkBookmark | null) { - set({ bookmarkToBeDeleted: bookmark }) - }, -})) - -function Page() { +function RouteComponent() { const setLayoutMode = useBookmarkPageStore((state) => state.setLayoutMode) useEffect(() => { function mediaQueryListener(this: MediaQueryList) { + console.log(this.matches) if (this.matches) { - setLayoutMode(LAYOUT_MODE.sideBySide) + setLayoutMode(LayoutMode.SideBySide) } else { - setLayoutMode(LAYOUT_MODE.popup) + setLayoutMode(LayoutMode.Popup) } } @@ -134,61 +39,11 @@ function Page() { return (
-
- - - -
+
) } - -function Main({ children }: React.PropsWithChildren) { - const isPreviewOpened = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened) - const layoutMode = useBookmarkPageStore((state) => state.layoutMode) - - return ( -
- {children} -
- ) -} - -function BookmarkListPane() { - const isBookmarkPreviewOpened = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened) - - return ( -
-
-

- -  >  - - YOUR BOOKMARKS -

-
-
- -
-
- ) -} - function PageDialog() { const dialog = useBookmarkPageStore((state) => state.activeDialog) switch (dialog) { @@ -201,90 +56,6 @@ function PageDialog() { } } -function DeleteBookmarkDialog() { - // biome-ignore lint/style/noNonNullAssertion: this cannot be null when delete bookmark dialog is visible - const bookmark = useBookmarkPageStore((state) => state.bookmarkToBeDeleted!) - const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog) - const markBookmarkForDeletion = useBookmarkPageStore((state) => state.markBookmarkForDeletion) - const deleteBookmarkMutation = useDeleteBookmark() - - useMnemonics( - { - y: proceed, - n: cancel, - }, - { ignore: () => false }, - ) - - async function proceed() { - try { - await deleteBookmarkMutation.mutateAsync({ bookmark }) - setActiveDialog(ActiveDialog.None) - markBookmarkForDeletion(null) - } catch (error) { - console.error(error) - } - } - - function cancel() { - setActiveDialog(ActiveDialog.None) - markBookmarkForDeletion(null) - } - - function body() { - switch (deleteBookmarkMutation.status) { - case "pending": - return ( -

- Deleting -

- ) - case "idle": - return ( -

- The bookmark titled: -
-
- - "{bookmark.title}" - -
-
- will be deleted. Proceed? -

- ) - case "error": - return

Failed to delete the bookmark!

- } - } - - function title() { - switch (deleteBookmarkMutation.status) { - case "pending": - return "PLEASE WAIT" - case "idle": - return "CONFIRM" - case "error": - return "ERROR OCCURRED" - } - } - - return ( - - {title()} - {body()} - - - - - - ) -} - function AddBookmarkDialog() { const [isWebsiteUnreachable, setIsWebsiteUnreachable] = useState(false) const createBookmarkMutation = useCreateBookmark() @@ -390,367 +161,86 @@ function AddBookmarkDialog() { ) } -function BookmarkListSection() { - const { data: bookmarks, status } = useAuthenticatedQuery(["bookmarks"], () => - fetchApi("/bookmarks").then((res) => res.json()), - ) - - switch (status) { - case "pending": - return ( -

- Loading  - -

- ) - case "success": - if (bookmarks) { - return - } - return null - default: - return null - } -} - -function BookmarkList({ bookmarks }: { bookmarks: LinkBookmark[] }) { - const reconcileSelection = useBookmarkPageStore((state) => state.reconcileSelection) - - useEffect(() => { - reconcileSelection(bookmarks) - }, [bookmarks, reconcileSelection]) - - useMnemonics( - { - j: selectNextItem, - ArrowDown: selectNextItem, - - k: selectPrevItem, - ArrowUp: selectPrevItem, - - h: collapseItem, - ArrowLeft: collapseItem, - - l: expandItem, - ArrowRight: expandItem, - - Enter: () => { - const state = useBookmarkPageStore.getState() - expandItem() - state.setBookmarkPreviewOpened(true) - }, - }, - { - ignore: useCallback(() => useBookmarkPageStore.getState().activeDialog !== ActiveDialog.None, []), - }, - ) - - function selectPrevItem() { - const state = useBookmarkPageStore.getState() - const prevIndex = state.selectedBookmarkIndex - 1 - if (prevIndex >= 0) { - state.selectBookmark(bookmarks[prevIndex], prevIndex) - } - } - - function selectNextItem() { - const state = useBookmarkPageStore.getState() - const nextIndex = state.selectedBookmarkIndex + 1 - if (nextIndex < bookmarks.length) { - state.selectBookmark(bookmarks[nextIndex], nextIndex) - } - } - - function expandItem() { - const state = useBookmarkPageStore.getState() - state.setBookmarkItemExpanded(true) - } - - function collapseItem() { - const state = useBookmarkPageStore.getState() - state.setBookmarkItemExpanded(false) - state.setBookmarkPreviewOpened(false) - } - - return ( -
- {bookmarks.length === 0 ? ( -

You have not saved any bookmark!

- ) : ( - bookmarks.map((bookmark, i) => ) - )} -
- ) -} - -function BookmarkPreview() { - const isVisible = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened) - const layoutMode = useBookmarkPageStore((state) => state.layoutMode) - - if (!isVisible) { - return null - } - - return ( -
- -
- ) -} - -function BookmarkPreviewContent() { - const selectedBookmarkId = useBookmarkPageStore((state) => state.selectedBookmarkId) - const actionBarHeight = useBookmarkPageStore((state) => state.actionBarHeight) - const { data, status } = useAuthenticatedQuery(["bookmarks", `${selectedBookmarkId}.html`], () => - fetchApi(`/bookmark/${selectedBookmarkId}`, { - headers: { - Accept: "text/html", - }, - }).then((res) => res.text()), - ) - - switch (status) { - case "pending": - return ( -

- Loading -

- ) - case "success": - return ( -