move bookmark preview to its own route
This commit is contained in:
@@ -15,6 +15,8 @@ import { Route as SignupImport } from "./signup"
|
|||||||
import { Route as LoginImport } from "./login"
|
import { Route as LoginImport } from "./login"
|
||||||
import { Route as BookmarksImport } from "./bookmarks"
|
import { Route as BookmarksImport } from "./bookmarks"
|
||||||
import { Route as IndexImport } from "./index"
|
import { Route as IndexImport } from "./index"
|
||||||
|
import { Route as BookmarksIndexImport } from "./bookmarks/index"
|
||||||
|
import { Route as BookmarksBookmarkIdImport } from "./bookmarks/$bookmarkId"
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
@@ -42,6 +44,18 @@ const IndexRoute = IndexImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} 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
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
declare module "@tanstack/react-router" {
|
declare module "@tanstack/react-router" {
|
||||||
@@ -74,52 +88,98 @@ declare module "@tanstack/react-router" {
|
|||||||
preLoaderRoute: typeof SignupImport
|
preLoaderRoute: typeof SignupImport
|
||||||
parentRoute: typeof rootRoute
|
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
|
// 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 {
|
export interface FileRoutesByFullPath {
|
||||||
"/": typeof IndexRoute
|
"/": typeof IndexRoute
|
||||||
"/bookmarks": typeof BookmarksRoute
|
"/bookmarks": typeof BookmarksRouteWithChildren
|
||||||
"/login": typeof LoginRoute
|
"/login": typeof LoginRoute
|
||||||
"/signup": typeof SignupRoute
|
"/signup": typeof SignupRoute
|
||||||
|
"/bookmarks/$bookmarkId": typeof BookmarksBookmarkIdRoute
|
||||||
|
"/bookmarks/": typeof BookmarksIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
"/": typeof IndexRoute
|
"/": typeof IndexRoute
|
||||||
"/bookmarks": typeof BookmarksRoute
|
|
||||||
"/login": typeof LoginRoute
|
"/login": typeof LoginRoute
|
||||||
"/signup": typeof SignupRoute
|
"/signup": typeof SignupRoute
|
||||||
|
"/bookmarks/$bookmarkId": typeof BookmarksBookmarkIdRoute
|
||||||
|
"/bookmarks": typeof BookmarksIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRoute
|
__root__: typeof rootRoute
|
||||||
"/": typeof IndexRoute
|
"/": typeof IndexRoute
|
||||||
"/bookmarks": typeof BookmarksRoute
|
"/bookmarks": typeof BookmarksRouteWithChildren
|
||||||
"/login": typeof LoginRoute
|
"/login": typeof LoginRoute
|
||||||
"/signup": typeof SignupRoute
|
"/signup": typeof SignupRoute
|
||||||
|
"/bookmarks/$bookmarkId": typeof BookmarksBookmarkIdRoute
|
||||||
|
"/bookmarks/": typeof BookmarksIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: "/" | "/bookmarks" | "/login" | "/signup"
|
fullPaths:
|
||||||
|
| "/"
|
||||||
|
| "/bookmarks"
|
||||||
|
| "/login"
|
||||||
|
| "/signup"
|
||||||
|
| "/bookmarks/$bookmarkId"
|
||||||
|
| "/bookmarks/"
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: "/" | "/bookmarks" | "/login" | "/signup"
|
to: "/" | "/login" | "/signup" | "/bookmarks/$bookmarkId" | "/bookmarks"
|
||||||
id: "__root__" | "/" | "/bookmarks" | "/login" | "/signup"
|
id:
|
||||||
|
| "__root__"
|
||||||
|
| "/"
|
||||||
|
| "/bookmarks"
|
||||||
|
| "/login"
|
||||||
|
| "/signup"
|
||||||
|
| "/bookmarks/$bookmarkId"
|
||||||
|
| "/bookmarks/"
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
BookmarksRoute: typeof BookmarksRoute
|
BookmarksRoute: typeof BookmarksRouteWithChildren
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
SignupRoute: typeof SignupRoute
|
SignupRoute: typeof SignupRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
BookmarksRoute: BookmarksRoute,
|
BookmarksRoute: BookmarksRouteWithChildren,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
SignupRoute: SignupRoute,
|
SignupRoute: SignupRoute,
|
||||||
}
|
}
|
||||||
@@ -144,13 +204,25 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "index.tsx"
|
"filePath": "index.tsx"
|
||||||
},
|
},
|
||||||
"/bookmarks": {
|
"/bookmarks": {
|
||||||
"filePath": "bookmarks.tsx"
|
"filePath": "bookmarks.tsx",
|
||||||
|
"children": [
|
||||||
|
"/bookmarks/$bookmarkId",
|
||||||
|
"/bookmarks/"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"/login": {
|
"/login": {
|
||||||
"filePath": "login.tsx"
|
"filePath": "login.tsx"
|
||||||
},
|
},
|
||||||
"/signup": {
|
"/signup": {
|
||||||
"filePath": "signup.tsx"
|
"filePath": "signup.tsx"
|
||||||
|
},
|
||||||
|
"/bookmarks/$bookmarkId": {
|
||||||
|
"filePath": "bookmarks/$bookmarkId.tsx",
|
||||||
|
"parent": "/bookmarks"
|
||||||
|
},
|
||||||
|
"/bookmarks/": {
|
||||||
|
"filePath": "bookmarks/index.tsx",
|
||||||
|
"parent": "/bookmarks"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,124 +1,29 @@
|
|||||||
import type { LinkBookmark } from "@markone/core/bookmark"
|
import { Outlet, createFileRoute } from "@tanstack/react-router"
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
import { useState, useId, useRef, useEffect } from "react"
|
||||||
import clsx from "clsx"
|
import { BadRequestError, ApiErrorCode } from "~/api"
|
||||||
import { useCallback, useEffect, useId, useRef, useState } from "react"
|
|
||||||
import { create } from "zustand"
|
|
||||||
import { fetchApi, useAuthenticatedQuery, BadRequestError, ApiErrorCode } from "~/api"
|
|
||||||
import { useCreateBookmark, useDeleteBookmark } from "~/bookmark/api"
|
import { useCreateBookmark, useDeleteBookmark } from "~/bookmark/api"
|
||||||
import { Button } from "~/components/button"
|
import { Button } from "~/components/button"
|
||||||
import { Dialog, DialogActionRow, DialogBody, DialogTitle } from "~/components/dialog"
|
import { Dialog, DialogTitle, DialogBody, DialogActionRow } from "~/components/dialog"
|
||||||
import { Message, MessageVariant } from "~/components/message"
|
|
||||||
import { FormField } from "~/components/form-field"
|
import { FormField } from "~/components/form-field"
|
||||||
import { LoadingSpinner } from "~/components/loading-spinner"
|
import { LoadingSpinner } from "~/components/loading-spinner"
|
||||||
|
import { Message, MessageVariant } from "~/components/message"
|
||||||
import { useMnemonics } from "~/hooks/use-mnemonics"
|
import { useMnemonics } from "~/hooks/use-mnemonics"
|
||||||
import { useLogOut } from "~/auth"
|
import { useBookmarkPageStore, ActiveDialog, LayoutMode } from "./bookmarks/-store"
|
||||||
|
|
||||||
const LAYOUT_MODE = {
|
export const Route = createFileRoute("/bookmarks")({
|
||||||
popup: "popup",
|
component: RouteComponent,
|
||||||
sideBySide: "side-by-side",
|
})
|
||||||
} as const
|
|
||||||
type LayoutMode = (typeof LAYOUT_MODE)[keyof typeof LAYOUT_MODE]
|
|
||||||
|
|
||||||
enum ActiveDialog {
|
function RouteComponent() {
|
||||||
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<BookmarkPageState>()((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() {
|
|
||||||
const setLayoutMode = useBookmarkPageStore((state) => state.setLayoutMode)
|
const setLayoutMode = useBookmarkPageStore((state) => state.setLayoutMode)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function mediaQueryListener(this: MediaQueryList) {
|
function mediaQueryListener(this: MediaQueryList) {
|
||||||
|
console.log(this.matches)
|
||||||
if (this.matches) {
|
if (this.matches) {
|
||||||
setLayoutMode(LAYOUT_MODE.sideBySide)
|
setLayoutMode(LayoutMode.SideBySide)
|
||||||
} else {
|
} else {
|
||||||
setLayoutMode(LAYOUT_MODE.popup)
|
setLayoutMode(LayoutMode.Popup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,61 +39,11 @@ function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Main>
|
<Outlet />
|
||||||
<BookmarkListPane />
|
|
||||||
<BookmarkPreview />
|
|
||||||
<ActionBar />
|
|
||||||
</Main>
|
|
||||||
<PageDialog />
|
<PageDialog />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Main({ children }: React.PropsWithChildren) {
|
|
||||||
const isPreviewOpened = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened)
|
|
||||||
const layoutMode = useBookmarkPageStore((state) => state.layoutMode)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main
|
|
||||||
className={clsx(
|
|
||||||
"relative w-full",
|
|
||||||
isPreviewOpened && layoutMode === LAYOUT_MODE.sideBySide ? "grid grid-cols-3" : "flex justify-center",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BookmarkListPane() {
|
|
||||||
const isBookmarkPreviewOpened = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"flex flex-col py-16",
|
|
||||||
isBookmarkPreviewOpened ? "w-full" : "container max-w-3xl md:flex-row lg:py-32",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<header className="mb-4 md:mb-0 md:mr-16 text-start">
|
|
||||||
<h1 className={clsx("font-bold text-start", { "mb-4": isBookmarkPreviewOpened })}>
|
|
||||||
<span
|
|
||||||
className={clsx("invisible", {
|
|
||||||
"md:hidden": !isBookmarkPreviewOpened,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
YOUR BOOKMARKS
|
|
||||||
</h1>
|
|
||||||
</header>
|
|
||||||
<div className="flex-1">
|
|
||||||
<BookmarkListSection />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PageDialog() {
|
function PageDialog() {
|
||||||
const dialog = useBookmarkPageStore((state) => state.activeDialog)
|
const dialog = useBookmarkPageStore((state) => state.activeDialog)
|
||||||
switch (dialog) {
|
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 (
|
|
||||||
<p>
|
|
||||||
Deleting <LoadingSpinner />
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
case "idle":
|
|
||||||
return (
|
|
||||||
<p>
|
|
||||||
The bookmark titled:
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<strong>
|
|
||||||
<em>"{bookmark.title}"</em>
|
|
||||||
</strong>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
will be deleted. Proceed?
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
case "error":
|
|
||||||
return <p className="text-red-500">Failed to delete the bookmark!</p>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function title() {
|
|
||||||
switch (deleteBookmarkMutation.status) {
|
|
||||||
case "pending":
|
|
||||||
return "PLEASE WAIT"
|
|
||||||
case "idle":
|
|
||||||
return "CONFIRM"
|
|
||||||
case "error":
|
|
||||||
return "ERROR OCCURRED"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTitle>{title()}</DialogTitle>
|
|
||||||
<DialogBody>{body()}</DialogBody>
|
|
||||||
<DialogActionRow>
|
|
||||||
<Button disabled={deleteBookmarkMutation.isPending} onClick={proceed}>
|
|
||||||
{deleteBookmarkMutation.isError ? "Retry" : "Proceed"} (y)
|
|
||||||
</Button>
|
|
||||||
<Button disabled={deleteBookmarkMutation.isPending} onClick={cancel}>
|
|
||||||
Cancel (n)
|
|
||||||
</Button>
|
|
||||||
</DialogActionRow>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AddBookmarkDialog() {
|
function AddBookmarkDialog() {
|
||||||
const [isWebsiteUnreachable, setIsWebsiteUnreachable] = useState(false)
|
const [isWebsiteUnreachable, setIsWebsiteUnreachable] = useState(false)
|
||||||
const createBookmarkMutation = useCreateBookmark()
|
const createBookmarkMutation = useCreateBookmark()
|
||||||
@@ -390,367 +161,86 @@ function AddBookmarkDialog() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BookmarkListSection() {
|
function DeleteBookmarkDialog() {
|
||||||
const { data: bookmarks, status } = useAuthenticatedQuery(["bookmarks"], () =>
|
// biome-ignore lint/style/noNonNullAssertion: this cannot be null when delete bookmark dialog is visible
|
||||||
fetchApi("/bookmarks").then((res) => res.json()),
|
const bookmark = useBookmarkPageStore((state) => state.bookmarkToBeDeleted!)
|
||||||
)
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case "pending":
|
|
||||||
return (
|
|
||||||
<p>
|
|
||||||
Loading
|
|
||||||
<LoadingSpinner />
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
case "success":
|
|
||||||
if (bookmarks) {
|
|
||||||
return <BookmarkList bookmarks={bookmarks} />
|
|
||||||
}
|
|
||||||
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 (
|
|
||||||
<div className="flex flex-col -mt-2">
|
|
||||||
{bookmarks.length === 0 ? (
|
|
||||||
<p className="mt-2">You have not saved any bookmark!</p>
|
|
||||||
) : (
|
|
||||||
bookmarks.map((bookmark, i) => <BookmarkListItem key={bookmark.id} index={i} bookmark={bookmark} />)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BookmarkPreview() {
|
|
||||||
const isVisible = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened)
|
|
||||||
const layoutMode = useBookmarkPageStore((state) => state.layoutMode)
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"h-screen col-span-2 flex justify-center items-center border-l border-stone-700 dark:border-stone-300 flex dark:bg-stone-900",
|
|
||||||
{
|
|
||||||
"absolute inset-0 border-l-0": layoutMode === LAYOUT_MODE.popup,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<BookmarkPreviewContent />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<p>
|
|
||||||
Loading <LoadingSpinner />
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
case "success":
|
|
||||||
return (
|
|
||||||
<iframe
|
|
||||||
key="preview-iframe"
|
|
||||||
title="asd"
|
|
||||||
className={clsx("w-full h-full")}
|
|
||||||
style={{ paddingBottom: actionBarHeight }}
|
|
||||||
srcDoc={data}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function BookmarkListItem({ bookmark, index }: { bookmark: LinkBookmark; index: number }) {
|
|
||||||
const url = new URL(bookmark.url)
|
|
||||||
const selectedBookmarkIndex = useBookmarkPageStore((state) => state.selectedBookmarkIndex)
|
|
||||||
const isBookmarkItemExpanded = useBookmarkPageStore((state) => state.isBookmarkItemExpanded)
|
|
||||||
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
|
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
|
||||||
const setBookmarkItemExpanded = useBookmarkPageStore((state) => state.setBookmarkItemExpanded)
|
|
||||||
const selectBookmark = useBookmarkPageStore((state) => state.selectBookmark)
|
|
||||||
const setBookmarkPreviewOpened = useBookmarkPageStore((state) => state.setBookmarkPreviewOpened)
|
|
||||||
const markBookmarkForDeletion = useBookmarkPageStore((state) => state.markBookmarkForDeletion)
|
const markBookmarkForDeletion = useBookmarkPageStore((state) => state.markBookmarkForDeletion)
|
||||||
const isSelected = selectedBookmarkIndex === index
|
const deleteBookmarkMutation = useDeleteBookmark()
|
||||||
|
|
||||||
useMnemonics(
|
useMnemonics(
|
||||||
{
|
{
|
||||||
d: deleteItem,
|
y: proceed,
|
||||||
},
|
n: cancel,
|
||||||
{
|
|
||||||
ignore: useCallback(
|
|
||||||
() => !isSelected || useBookmarkPageStore.getState().activeDialog !== ActiveDialog.None,
|
|
||||||
[isSelected],
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
{ ignore: () => false },
|
||||||
)
|
)
|
||||||
|
|
||||||
function deleteItem() {
|
async function proceed() {
|
||||||
if (isSelected) {
|
try {
|
||||||
markBookmarkForDeletion(bookmark)
|
await deleteBookmarkMutation.mutateAsync({ bookmark })
|
||||||
setActiveDialog(ActiveDialog.DeleteBookmark)
|
setActiveDialog(ActiveDialog.None)
|
||||||
|
markBookmarkForDeletion(null)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function expandOrOpenPreview() {
|
function cancel() {
|
||||||
if (!isSelected) {
|
setActiveDialog(ActiveDialog.None)
|
||||||
selectBookmark(bookmark, index)
|
markBookmarkForDeletion(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function body() {
|
||||||
|
switch (deleteBookmarkMutation.status) {
|
||||||
|
case "pending":
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Deleting <LoadingSpinner />
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
case "idle":
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
The bookmark titled:
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<strong>
|
||||||
|
<em>"{bookmark.title}"</em>
|
||||||
|
</strong>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
will be deleted. Proceed?
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
case "error":
|
||||||
|
return <p className="text-red-500">Failed to delete the bookmark!</p>
|
||||||
}
|
}
|
||||||
setBookmarkItemExpanded(true)
|
}
|
||||||
if (useBookmarkPageStore.getState().layoutMode === LAYOUT_MODE.sideBySide) {
|
|
||||||
setBookmarkPreviewOpened(true)
|
function title() {
|
||||||
|
switch (deleteBookmarkMutation.status) {
|
||||||
|
case "pending":
|
||||||
|
return "PLEASE WAIT"
|
||||||
|
case "idle":
|
||||||
|
return "CONFIRM"
|
||||||
|
case "error":
|
||||||
|
return "ERROR OCCURRED"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Dialog>
|
||||||
className={clsx("group flex flex-row justify-start py-2", {
|
<DialogTitle>{title()}</DialogTitle>
|
||||||
"bg-teal-600 text-stone-100": isBookmarkItemExpanded && isSelected,
|
<DialogBody>{body()}</DialogBody>
|
||||||
"text-teal-600": isSelected && !isBookmarkItemExpanded,
|
<DialogActionRow>
|
||||||
})}
|
<Button disabled={deleteBookmarkMutation.isPending} onClick={proceed}>
|
||||||
onMouseEnter={() => {
|
{deleteBookmarkMutation.isError ? "Retry" : "Proceed"} (y)
|
||||||
if (!isBookmarkItemExpanded) {
|
</Button>
|
||||||
selectBookmark(bookmark, index)
|
<Button disabled={deleteBookmarkMutation.isPending} onClick={cancel}>
|
||||||
}
|
Cancel (n)
|
||||||
}}
|
</Button>
|
||||||
>
|
</DialogActionRow>
|
||||||
<button
|
</Dialog>
|
||||||
type="button"
|
|
||||||
disabled={!isSelected}
|
|
||||||
className={clsx("select-none flex items-start font-bold hover:bg-teal-600 hover:text-stone-100", {
|
|
||||||
invisible: !isSelected,
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
setBookmarkItemExpanded(!isBookmarkItemExpanded)
|
|
||||||
setBookmarkPreviewOpened(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Options for this bookmark</span>
|
|
||||||
<span> </span>
|
|
||||||
<span className={isBookmarkItemExpanded ? "rotate-90" : ""}>></span>
|
|
||||||
<span> </span>
|
|
||||||
</button>
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<button type="button" className="text-start font-bold" onClick={expandOrOpenPreview}>
|
|
||||||
{bookmark.title}
|
|
||||||
</button>
|
|
||||||
<p className="opacity-80 text-sm">{url.host}</p>
|
|
||||||
{isBookmarkItemExpanded && isSelected ? (
|
|
||||||
<div className="flex flex-col space-y-1 md:flex-row md:space-y-0 md:space-x-2 items-end justify-between pt-2">
|
|
||||||
<p className="text-sm">#dev</p>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<OpenBookmarkPreviewButton />
|
|
||||||
<Button variant="light" className="text-sm">
|
|
||||||
<span className="underline">E</span>dit
|
|
||||||
</Button>
|
|
||||||
<Button variant="light" className="text-sm" onClick={deleteItem}>
|
|
||||||
<span className="underline">D</span>elete
|
|
||||||
</Button>
|
|
||||||
<span className="-ml-2"> </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function OpenBookmarkPreviewButton() {
|
|
||||||
const isBookmarkPreviewOpened = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened)
|
|
||||||
const setBookmarkPreviewOpened = useBookmarkPageStore((state) => state.setBookmarkPreviewOpened)
|
|
||||||
const setBookmarkItemExpanded = useBookmarkPageStore((state) => state.setBookmarkItemExpanded)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function onKeyDown(event: KeyboardEvent) {
|
|
||||||
if (isBookmarkPreviewOpened && event.key === "c") {
|
|
||||||
closePreview()
|
|
||||||
} else if (!isBookmarkPreviewOpened && event.key === "o") {
|
|
||||||
openPreview()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("keydown", onKeyDown)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", onKeyDown)
|
|
||||||
}
|
|
||||||
}, [isBookmarkPreviewOpened])
|
|
||||||
|
|
||||||
function closePreview() {
|
|
||||||
setBookmarkPreviewOpened(false)
|
|
||||||
setBookmarkItemExpanded(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function openPreview() {
|
|
||||||
setBookmarkPreviewOpened(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="light"
|
|
||||||
className="text-sm"
|
|
||||||
onClick={() => {
|
|
||||||
if (isBookmarkPreviewOpened) {
|
|
||||||
closePreview()
|
|
||||||
} else {
|
|
||||||
openPreview()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isBookmarkPreviewOpened ? (
|
|
||||||
<>
|
|
||||||
<span className="underline">C</span>lose
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="underline">O</span>pen
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ActionBar() {
|
|
||||||
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
|
|
||||||
const setActionBarHeight = useBookmarkPageStore((state) => state.setActionBarHeight)
|
|
||||||
const layoutMode = useBookmarkPageStore((state) => state.layoutMode)
|
|
||||||
const isBookmarkPreviewOpened = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened)
|
|
||||||
const setBookmarkPreviewOpened = useBookmarkPageStore((state) => state.setBookmarkPreviewOpened)
|
|
||||||
|
|
||||||
useMnemonics(
|
|
||||||
{
|
|
||||||
a: addBookmark,
|
|
||||||
},
|
|
||||||
{ ignore: useCallback(() => useBookmarkPageStore.getState().activeDialog !== ActiveDialog.None, []) },
|
|
||||||
)
|
|
||||||
|
|
||||||
function addBookmark() {
|
|
||||||
setActiveDialog(ActiveDialog.AddBookmark)
|
|
||||||
}
|
|
||||||
|
|
||||||
function closePreview() {
|
|
||||||
setBookmarkPreviewOpened(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={(el) => {
|
|
||||||
if (el) {
|
|
||||||
setActionBarHeight(el.clientHeight)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="fixed z-10 bottom-0 left-0 right-0 bg-stone-200 dark:bg-stone-900 border-t-1 flex flex-row justify-center py-4 space-x-4"
|
|
||||||
>
|
|
||||||
{layoutMode === LAYOUT_MODE.popup && isBookmarkPreviewOpened ? (
|
|
||||||
<Button onClick={closePreview}>CLOSE</Button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Button onClick={addBookmark}>
|
|
||||||
<span className="underline">A</span>DD
|
|
||||||
</Button>
|
|
||||||
<Button>
|
|
||||||
<span className="underline">S</span>EARCH
|
|
||||||
</Button>
|
|
||||||
<LogOutButton />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/bookmarks")({
|
|
||||||
component: Page,
|
|
||||||
})
|
|
||||||
|
126
packages/web/src/app/bookmarks/$bookmarkId.tsx
Normal file
126
packages/web/src/app/bookmarks/$bookmarkId.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
||||||
|
import { LayoutMode, useBookmarkPageStore } from "./-store"
|
||||||
|
import clsx from "clsx"
|
||||||
|
import { fetchApi, useAuthenticatedQuery } from "~/api"
|
||||||
|
import { LoadingSpinner } from "~/components/loading-spinner"
|
||||||
|
import { BookmarkList } from "./-bookmark-list"
|
||||||
|
import { useCallback } from "react"
|
||||||
|
import type { LinkBookmark } from "@markone/core/bookmark"
|
||||||
|
import { ActionBar } from "./-action-bar"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/bookmarks/$bookmarkId")({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return (
|
||||||
|
<Main>
|
||||||
|
<BookmarkListSidebar />
|
||||||
|
<BookmarkPreviewContainer>
|
||||||
|
<BookmarkPreview />
|
||||||
|
</BookmarkPreviewContainer>
|
||||||
|
</Main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Main({ children }: React.PropsWithChildren) {
|
||||||
|
const layoutMode = useBookmarkPageStore((state) => state.layoutMode)
|
||||||
|
return (
|
||||||
|
<main className={clsx("w-full h-full grid", layoutMode === LayoutMode.SideBySide ? "grid-cols-4" : "grid-cols-1")}>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BookmarkPreviewContainer({ children }: React.PropsWithChildren) {
|
||||||
|
const layoutMode = useBookmarkPageStore((state) => state.layoutMode)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx("col-span-3 h-screen border-l border-stone-700 dark:border-stone-300 flex dark:bg-stone-900", {
|
||||||
|
"absolute border-l-0": layoutMode === LayoutMode.Popup,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BookmarkListSidebar() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col py-16 w-full h-screen relative">
|
||||||
|
<header className="mb-4 text-start">
|
||||||
|
<h1 className="font-bold text-start mb-4">
|
||||||
|
<span className="invisible"> > </span>
|
||||||
|
YOUR BOOKMARKS
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<BookmarkListContainer />
|
||||||
|
<ActionBar className="absolute bottom-0 left-0 right-0" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BookmarkListContainer() {
|
||||||
|
const { bookmarkId } = Route.useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { data: bookmarks, status } = useAuthenticatedQuery(["bookmarks"], () =>
|
||||||
|
fetchApi("/bookmarks").then((res) => res.json()),
|
||||||
|
)
|
||||||
|
const handleBookmarkListItemAction = useBookmarkPageStore((state) => state.handleBookmarkListItemAction)
|
||||||
|
|
||||||
|
const onSelectedBookmarkChange = useCallback(
|
||||||
|
(bookmark: LinkBookmark) => {
|
||||||
|
navigate({ to: `/bookmarks/${bookmark.id}` })
|
||||||
|
},
|
||||||
|
[navigate],
|
||||||
|
)
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "success":
|
||||||
|
return (
|
||||||
|
<BookmarkList
|
||||||
|
alwaysExpandItem
|
||||||
|
bookmarks={bookmarks}
|
||||||
|
selectedBookmarkId={bookmarkId}
|
||||||
|
onSelectionChange={onSelectedBookmarkChange}
|
||||||
|
onItemAction={handleBookmarkListItemAction}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case "pending":
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Loading <LoadingSpinner />
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
|
||||||
|
case "error":
|
||||||
|
return <p>error loading bookmarks</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function BookmarkPreview() {
|
||||||
|
const { bookmarkId } = Route.useParams()
|
||||||
|
const { data, status } = useAuthenticatedQuery(["bookmarks", `${bookmarkId}.html`], () =>
|
||||||
|
fetchApi(`/bookmark/${bookmarkId}`, {
|
||||||
|
headers: {
|
||||||
|
Accept: "text/html",
|
||||||
|
},
|
||||||
|
}).then((res) => res.text()),
|
||||||
|
)
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "pending":
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Loading <LoadingSpinner />
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
case "success":
|
||||||
|
return <iframe key="preview-iframe" title="asd" className="w-full h-full" srcDoc={data} />
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
57
packages/web/src/app/bookmarks/-action-bar.tsx
Normal file
57
packages/web/src/app/bookmarks/-action-bar.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Button } from "~/components/button"
|
||||||
|
import { ActiveDialog, LayoutMode, useBookmarkPageStore } from "./-store"
|
||||||
|
import { useNavigate } from "@tanstack/react-router"
|
||||||
|
import { useCallback } from "react"
|
||||||
|
import { useLogOut } from "~/auth"
|
||||||
|
import { useMnemonics } from "~/hooks/use-mnemonics"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
function ActionBar({ className }: { className?: string }) {
|
||||||
|
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
|
||||||
|
|
||||||
|
useMnemonics(
|
||||||
|
{
|
||||||
|
a: addBookmark,
|
||||||
|
},
|
||||||
|
{ ignore: useCallback(() => useBookmarkPageStore.getState().activeDialog !== ActiveDialog.None, []) },
|
||||||
|
)
|
||||||
|
|
||||||
|
function addBookmark() {
|
||||||
|
setActiveDialog(ActiveDialog.AddBookmark)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"bg-stone-200 dark:bg-stone-900 border-t-1 flex flex-row justify-center py-4 space-x-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button onClick={addBookmark}>
|
||||||
|
<span className="underline">A</span>DD
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
<span className="underline">S</span>EARCH
|
||||||
|
</Button>
|
||||||
|
<LogOutButton />
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ActionBar }
|
376
packages/web/src/app/bookmarks/-bookmark-list.tsx
Normal file
376
packages/web/src/app/bookmarks/-bookmark-list.tsx
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import type { LinkBookmark } from "@markone/core/bookmark"
|
||||||
|
import { Link } from "@tanstack/react-router"
|
||||||
|
import { createStore, useStore } from "zustand"
|
||||||
|
import { useEffect, useCallback, createContext, useRef, memo, useContext } from "react"
|
||||||
|
import { useMnemonics } from "~/hooks/use-mnemonics"
|
||||||
|
import { useBookmarkPageStore, ActiveDialog, LayoutMode } from "./-store"
|
||||||
|
import { Button } from "~/components/button"
|
||||||
|
import clsx from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
import { subscribeWithSelector } from "zustand/middleware"
|
||||||
|
|
||||||
|
enum BookmarkListItemAction {
|
||||||
|
Open = "Open",
|
||||||
|
Edit = "Edit",
|
||||||
|
Delete = "Delete",
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectionChangeCallback = (bookmark: LinkBookmark) => void
|
||||||
|
type ItemActionCallback = (bookmark: LinkBookmark, action: BookmarkListItemAction) => void
|
||||||
|
|
||||||
|
interface BookmarkListProps {
|
||||||
|
bookmarks: LinkBookmark[]
|
||||||
|
selectedBookmarkId?: string
|
||||||
|
alwaysExpandItem: boolean
|
||||||
|
onSelectionChange?: SelectionChangeCallback
|
||||||
|
onItemAction: (bookmark: LinkBookmark, action: BookmarkListItemAction) => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateStoreOptions {
|
||||||
|
bookmarks: LinkBookmark[]
|
||||||
|
selectedBookmarkId?: string
|
||||||
|
alwaysExpandItem: boolean
|
||||||
|
onItemAction: ItemActionCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookmarkListState {
|
||||||
|
bookmarks: LinkBookmark[]
|
||||||
|
selectedIndex: number
|
||||||
|
selectedBookmarkId: string
|
||||||
|
alwaysExpandItem: boolean
|
||||||
|
isItemExpanded: boolean
|
||||||
|
|
||||||
|
onItemAction: ItemActionCallback
|
||||||
|
setBookmarks: (bookmarks: LinkBookmark[]) => void
|
||||||
|
setSelectedIndex: (index: number) => void
|
||||||
|
setSelectedBookmarkId: (id: string) => void
|
||||||
|
setIsItemExpanded: (expanded: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type BookmarkListStore = ReturnType<typeof createBookmarkListStore>
|
||||||
|
|
||||||
|
const BookmarkListStoreContext = createContext<BookmarkListStore | null>(null)
|
||||||
|
|
||||||
|
function createBookmarkListStore({
|
||||||
|
bookmarks,
|
||||||
|
selectedBookmarkId,
|
||||||
|
alwaysExpandItem,
|
||||||
|
onItemAction,
|
||||||
|
}: CreateStoreOptions) {
|
||||||
|
return createStore<BookmarkListState>()(
|
||||||
|
subscribeWithSelector((set) => ({
|
||||||
|
bookmarks,
|
||||||
|
alwaysExpandItem,
|
||||||
|
selectedIndex: selectedBookmarkId ? bookmarks.findIndex((bookmark) => bookmark.id === selectedBookmarkId) : 0,
|
||||||
|
selectedBookmarkId: selectedBookmarkId ?? bookmarks[0].id,
|
||||||
|
isItemExpanded: false,
|
||||||
|
|
||||||
|
onItemAction,
|
||||||
|
|
||||||
|
setBookmarks(bookmarks: LinkBookmark[]) {
|
||||||
|
set({ bookmarks })
|
||||||
|
},
|
||||||
|
|
||||||
|
setSelectedIndex(index: number) {
|
||||||
|
set({ selectedIndex: index })
|
||||||
|
},
|
||||||
|
|
||||||
|
setSelectedBookmarkId(id: string) {
|
||||||
|
set({ selectedBookmarkId: id })
|
||||||
|
},
|
||||||
|
|
||||||
|
setIsItemExpanded(expanded: boolean) {
|
||||||
|
set({ isItemExpanded: expanded })
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useBookmarkListStoreContext() {
|
||||||
|
const store = useContext(BookmarkListStoreContext)
|
||||||
|
if (!store) throw new Error("BookmarkListStoreContext not found")
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
function useBookmarkListStore<T>(selector: (state: BookmarkListState) => T): T {
|
||||||
|
const store = useBookmarkListStoreContext()
|
||||||
|
return useStore(store, selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BookmarkList({
|
||||||
|
bookmarks,
|
||||||
|
selectedBookmarkId,
|
||||||
|
alwaysExpandItem,
|
||||||
|
onSelectionChange,
|
||||||
|
onItemAction,
|
||||||
|
className,
|
||||||
|
}: BookmarkListProps) {
|
||||||
|
const storeRef = useRef<BookmarkListStore | null>(null)
|
||||||
|
if (!storeRef.current) {
|
||||||
|
storeRef.current = createBookmarkListStore({ bookmarks, selectedBookmarkId, alwaysExpandItem, onItemAction })
|
||||||
|
}
|
||||||
|
|
||||||
|
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!
|
||||||
|
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])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BookmarkListStoreContext value={storeRef.current}>
|
||||||
|
<_BookmarkList className={className} />
|
||||||
|
</BookmarkListStoreContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignore: useCallback(() => useBookmarkPageStore.getState().activeDialog !== ActiveDialog.None, []),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={twMerge("flex flex-col", className)}>
|
||||||
|
<ListContainer />
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function ListContainer() {
|
||||||
|
const bookmarks = useBookmarkListStore((state) => state.bookmarks)
|
||||||
|
const selectedItemId = useBookmarkListStore((state) => state.selectedBookmarkId)
|
||||||
|
|
||||||
|
return bookmarks.length === 0 ? (
|
||||||
|
<p className="mt-2">You have not saved any bookmark!</p>
|
||||||
|
) : (
|
||||||
|
bookmarks.map((bookmark, i) => (
|
||||||
|
<BookmarkListItem key={bookmark.id} index={i} bookmark={bookmark} selected={bookmark.id === selectedItemId} />
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const BookmarkListItem = memo(
|
||||||
|
({ bookmark, index, selected }: { bookmark: LinkBookmark; 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 setIsItemExpanded = useBookmarkListStore((state) => state.setIsItemExpanded)
|
||||||
|
const selectBookmark = useBookmarkPageStore((state) => state.selectBookmark)
|
||||||
|
const setBookmarkPreviewOpened = useBookmarkPageStore((state) => state.setBookmarkPreviewOpened)
|
||||||
|
const onItemAction = useBookmarkListStore((state) => state.onItemAction)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selected) {
|
||||||
|
store.getState().setSelectedIndex(index)
|
||||||
|
}
|
||||||
|
}, [selected, index, store.getState])
|
||||||
|
|
||||||
|
function deleteItem() {
|
||||||
|
onItemAction(bookmark, BookmarkListItemAction.Delete)
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandOrOpenPreview() {
|
||||||
|
if (!selected) {
|
||||||
|
selectBookmark(bookmark, index)
|
||||||
|
}
|
||||||
|
setIsItemExpanded(true)
|
||||||
|
if (useBookmarkPageStore.getState().layoutMode === LayoutMode.SideBySide) {
|
||||||
|
setBookmarkPreviewOpened(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={clsx("group flex flex-row justify-start py-2", {
|
||||||
|
"bg-teal-600 text-stone-100": isBookmarkItemExpanded && selected,
|
||||||
|
"text-teal-600": selected && !isBookmarkItemExpanded,
|
||||||
|
})}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (!isBookmarkItemExpanded) {
|
||||||
|
selectBookmark(bookmark, index)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={alwaysExpandItem || !selected}
|
||||||
|
className={clsx("select-none flex items-start font-bold hover:bg-teal-600 hover:text-stone-100", {
|
||||||
|
invisible: !selected,
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
setIsItemExpanded(!isBookmarkItemExpanded)
|
||||||
|
setBookmarkPreviewOpened(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Options for this bookmark</span>
|
||||||
|
<span> </span>
|
||||||
|
<span className={isBookmarkItemExpanded ? "rotate-90" : ""}>></span>
|
||||||
|
<span> </span>
|
||||||
|
</button>
|
||||||
|
<div className="flex flex-col w-full">
|
||||||
|
<Link
|
||||||
|
to={`/bookmarks/${bookmark.id}`}
|
||||||
|
className={clsx("block w-full text-start font-bold", { underline: selected })}
|
||||||
|
onClick={expandOrOpenPreview}
|
||||||
|
>
|
||||||
|
{bookmark.title}
|
||||||
|
</Link>
|
||||||
|
<p className="opacity-80 text-sm">{url.host}</p>
|
||||||
|
{isBookmarkItemExpanded && selected ? (
|
||||||
|
<div className="flex flex-col space-y-1 md:flex-row md:space-y-0 md:space-x-2 items-end justify-between pt-2">
|
||||||
|
<p className="text-sm">#dev</p>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<OpenBookmarkPreviewButton />
|
||||||
|
<Button variant="light" className="text-sm">
|
||||||
|
<span className="underline">E</span>dit
|
||||||
|
</Button>
|
||||||
|
<Button variant="light" className="text-sm" onClick={deleteItem}>
|
||||||
|
<span className="underline">D</span>elete
|
||||||
|
</Button>
|
||||||
|
<span className="-ml-2"> </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function OpenBookmarkPreviewButton() {
|
||||||
|
const isBookmarkPreviewOpened = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened)
|
||||||
|
const setBookmarkPreviewOpened = useBookmarkPageStore((state) => state.setBookmarkPreviewOpened)
|
||||||
|
const setBookmarkItemExpanded = useBookmarkPageStore((state) => state.setBookmarkItemExpanded)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
|
if (isBookmarkPreviewOpened && event.key === "c") {
|
||||||
|
closePreview()
|
||||||
|
} else if (!isBookmarkPreviewOpened && event.key === "o") {
|
||||||
|
openPreview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", onKeyDown)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", onKeyDown)
|
||||||
|
}
|
||||||
|
}, [isBookmarkPreviewOpened])
|
||||||
|
|
||||||
|
function closePreview() {
|
||||||
|
setBookmarkPreviewOpened(false)
|
||||||
|
setBookmarkItemExpanded(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPreview() {
|
||||||
|
setBookmarkPreviewOpened(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
className="text-sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (isBookmarkPreviewOpened) {
|
||||||
|
closePreview()
|
||||||
|
} else {
|
||||||
|
openPreview()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isBookmarkPreviewOpened ? (
|
||||||
|
<>
|
||||||
|
<span className="underline">C</span>lose
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="underline">O</span>pen
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BookmarkList, BookmarkListItemAction }
|
63
packages/web/src/app/bookmarks/-store.tsx
Normal file
63
packages/web/src/app/bookmarks/-store.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import type { LinkBookmark } from "@markone/core/bookmark"
|
||||||
|
import { create } from "zustand/react"
|
||||||
|
import { BookmarkListItemAction } from "./-bookmark-list"
|
||||||
|
import { router } from "~/router"
|
||||||
|
|
||||||
|
enum LayoutMode {
|
||||||
|
Popup = "Popup",
|
||||||
|
SideBySide = "SideBySide",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActiveDialog {
|
||||||
|
None = "None",
|
||||||
|
AddBookmark = "AddBookmark",
|
||||||
|
DeleteBookmark = "DeleteBookmark",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookmarkPageState {
|
||||||
|
bookmarkToBeDeleted: LinkBookmark | null
|
||||||
|
layoutMode: LayoutMode
|
||||||
|
activeDialog: ActiveDialog
|
||||||
|
|
||||||
|
handleBookmarkListItemAction: (bookmark: LinkBookmark, action: BookmarkListItemAction) => void
|
||||||
|
setActiveDialog: (dialog: ActiveDialog) => void
|
||||||
|
setLayoutMode: (mode: LayoutMode) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useBookmarkPageStore = create<BookmarkPageState>()((set, get) => ({
|
||||||
|
bookmarkCount: 0,
|
||||||
|
bookmarks: [],
|
||||||
|
selectedBookmarkId: "",
|
||||||
|
selectedBookmarkIndex: 0,
|
||||||
|
isBookmarkItemExpanded: false,
|
||||||
|
isBookmarkPreviewOpened: false,
|
||||||
|
bookmarkToBeDeleted: null,
|
||||||
|
layoutMode: LayoutMode.Popup,
|
||||||
|
activeDialog: ActiveDialog.None,
|
||||||
|
actionBarHeight: 0,
|
||||||
|
|
||||||
|
handleBookmarkListItemAction(bookmark: LinkBookmark, action: BookmarkListItemAction) {
|
||||||
|
switch (action) {
|
||||||
|
case BookmarkListItemAction.Open:
|
||||||
|
router.navigate({ to: `/bookmarks/${bookmark.id}` })
|
||||||
|
break
|
||||||
|
|
||||||
|
case BookmarkListItemAction.Delete:
|
||||||
|
set({ bookmarkToBeDeleted: bookmark, activeDialog: ActiveDialog.DeleteBookmark })
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setActiveDialog(dialog: ActiveDialog) {
|
||||||
|
set({ activeDialog: dialog })
|
||||||
|
},
|
||||||
|
|
||||||
|
setLayoutMode(mode: LayoutMode) {
|
||||||
|
set({ layoutMode: mode })
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
export { LayoutMode, ActiveDialog, useBookmarkPageStore }
|
||||||
|
export type { BookmarkPageState }
|
68
packages/web/src/app/bookmarks/index.tsx
Normal file
68
packages/web/src/app/bookmarks/index.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
||||||
|
import { useCallback } from "react"
|
||||||
|
import { useLogOut } from "~/auth"
|
||||||
|
import { Button } from "~/components/button"
|
||||||
|
import { useMnemonics } from "~/hooks/use-mnemonics"
|
||||||
|
import { BookmarkList } from "./-bookmark-list"
|
||||||
|
import { ActiveDialog, LayoutMode, useBookmarkPageStore } from "./-store"
|
||||||
|
import { fetchApi, useAuthenticatedQuery } from "~/api"
|
||||||
|
import { LoadingSpinner } from "~/components/loading-spinner"
|
||||||
|
import { ActionBar } from "./-action-bar"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/bookmarks/")({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return (
|
||||||
|
<main className="w-full flex justify-center">
|
||||||
|
<BookmarkListPane />
|
||||||
|
<ActionBar className="fixed left-0 right-0 bottom-0" />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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"> > </span>
|
||||||
|
YOUR BOOKMARKS
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<div className="flex-1">
|
||||||
|
<BookmarkListContainer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BookmarkListContainer() {
|
||||||
|
const { data: bookmarks, status } = useAuthenticatedQuery(["bookmarks"], () =>
|
||||||
|
fetchApi("/bookmarks").then((res) => res.json()),
|
||||||
|
)
|
||||||
|
const handleBookmarkListItemAction = useBookmarkPageStore((state) => state.handleBookmarkListItemAction)
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "success":
|
||||||
|
return (
|
||||||
|
<BookmarkList
|
||||||
|
className="-mt-2"
|
||||||
|
alwaysExpandItem={false}
|
||||||
|
bookmarks={bookmarks}
|
||||||
|
onItemAction={handleBookmarkListItemAction}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case "pending":
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Loading <LoadingSpinner />
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
|
||||||
|
case "error":
|
||||||
|
return <p>error loading bookmarks</p>
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react"
|
import { useEffect, useRef } from "react"
|
||||||
|
|
||||||
function useMnemonics(
|
function useMnemonics(
|
||||||
mnemonicMap: Record<string, (event: KeyboardEvent) => void>,
|
mnemonicMap: Record<string, (event: KeyboardEvent) => void>,
|
||||||
@@ -6,7 +6,6 @@ function useMnemonics(
|
|||||||
) {
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onKeyDown(event: KeyboardEvent) {
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
console.log(event.key)
|
|
||||||
if (!ignore(event)) {
|
if (!ignore(event)) {
|
||||||
mnemonicMap[event.key]?.(event)
|
mnemonicMap[event.key]?.(event)
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,9 @@
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||||
import { RouterProvider, createRouter } from "@tanstack/react-router"
|
import { RouterProvider } from "@tanstack/react-router"
|
||||||
import { StrictMode } from "react"
|
import { StrictMode } from "react"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
|
import { router } from "./router"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import { routeTree } from "./app/-route-tree.gen"
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
routeTree,
|
|
||||||
})
|
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
|
8
packages/web/src/router.ts
Normal file
8
packages/web/src/router.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createRouter } from "@tanstack/react-router"
|
||||||
|
import { routeTree } from "./app/-route-tree.gen"
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
routeTree,
|
||||||
|
})
|
||||||
|
|
||||||
|
export { router }
|
Reference in New Issue
Block a user