247 lines
6.0 KiB
TypeScript
247 lines
6.0 KiB
TypeScript
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, 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 { useBookmarkPageStore, ActiveDialog, LayoutMode } from "./bookmarks/-store"
|
|
|
|
export const Route = createFileRoute("/bookmarks")({
|
|
component: RouteComponent,
|
|
})
|
|
|
|
function RouteComponent() {
|
|
const setLayoutMode = useBookmarkPageStore((state) => state.setLayoutMode)
|
|
|
|
useEffect(() => {
|
|
function mediaQueryListener(this: MediaQueryList) {
|
|
console.log(this.matches)
|
|
if (this.matches) {
|
|
setLayoutMode(LayoutMode.SideBySide)
|
|
} else {
|
|
setLayoutMode(LayoutMode.Popup)
|
|
}
|
|
}
|
|
|
|
const q = window.matchMedia("(width >= 64rem)")
|
|
q.addEventListener("change", mediaQueryListener)
|
|
|
|
mediaQueryListener.call(q)
|
|
|
|
return () => {
|
|
q.removeEventListener("change", mediaQueryListener)
|
|
}
|
|
}, [setLayoutMode])
|
|
|
|
return (
|
|
<div className="relative">
|
|
<Outlet />
|
|
<PageDialog />
|
|
</div>
|
|
)
|
|
}
|
|
function PageDialog() {
|
|
const dialog = useBookmarkPageStore((state) => state.activeDialog)
|
|
switch (dialog) {
|
|
case ActiveDialog.None:
|
|
return null
|
|
case ActiveDialog.AddBookmark:
|
|
return <AddBookmarkDialog />
|
|
case ActiveDialog.DeleteBookmark:
|
|
return <DeleteBookmarkDialog />
|
|
}
|
|
}
|
|
|
|
function AddBookmarkDialog() {
|
|
const [isWebsiteUnreachable, setIsWebsiteUnreachable] = useState(false)
|
|
const createBookmarkMutation = useCreateBookmark()
|
|
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
|
|
const formId = useId()
|
|
const linkInputRef = useRef<HTMLInputElement | null>(null)
|
|
|
|
useMnemonics(
|
|
{
|
|
c: () => {
|
|
if (linkInputRef.current !== document.activeElement) {
|
|
cancel()
|
|
}
|
|
},
|
|
Escape: () => {
|
|
linkInputRef.current?.blur()
|
|
},
|
|
},
|
|
{ ignore: () => false },
|
|
)
|
|
|
|
useEffect(() => {
|
|
setTimeout(() => {
|
|
if (linkInputRef.current) {
|
|
linkInputRef.current.focus()
|
|
}
|
|
}, 0)
|
|
}, [])
|
|
|
|
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
|
|
event.preventDefault()
|
|
|
|
const formData = new FormData(event.currentTarget)
|
|
const url = formData.get("link")
|
|
if (url && typeof url === "string") {
|
|
try {
|
|
await createBookmarkMutation.mutateAsync({ url, force: isWebsiteUnreachable })
|
|
setActiveDialog(ActiveDialog.None)
|
|
} catch (error) {
|
|
if (error instanceof BadRequestError && error.code === ApiErrorCode.WebsiteUnreachable) {
|
|
setIsWebsiteUnreachable(true)
|
|
} else {
|
|
setIsWebsiteUnreachable(false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function cancel() {
|
|
setActiveDialog(ActiveDialog.None)
|
|
}
|
|
|
|
function message() {
|
|
if (createBookmarkMutation.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 (createBookmarkMutation.status === "error") {
|
|
return (
|
|
<Message variant={MessageVariant.Error} className="px-4">
|
|
An error occurred when saving bookmark
|
|
</Message>
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<Dialog>
|
|
<DialogTitle>NEW BOOKMARK</DialogTitle>
|
|
<DialogBody>
|
|
{message()}
|
|
<form id={formId} className="px-8" onSubmit={onSubmit}>
|
|
<FormField
|
|
ref={linkInputRef}
|
|
type="text"
|
|
name="link"
|
|
label="LINK"
|
|
className="w-full"
|
|
labelClassName="bg-stone-300 dark:bg-stone-800"
|
|
/>
|
|
</form>
|
|
</DialogBody>
|
|
<DialogActionRow>
|
|
<Button type="submit" disabled={createBookmarkMutation.isPending} form={formId}>
|
|
SAVE
|
|
</Button>
|
|
<Button type="button" disabled={createBookmarkMutation.isPending} onClick={cancel}>
|
|
<span className="underline">C</span>ANCEL
|
|
</Button>
|
|
</DialogActionRow>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
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>
|
|
)
|
|
}
|