handle bookmark delete loading state

This commit is contained in:
2025-05-07 16:57:09 +01:00
parent e87a6586b6
commit 9f00c9bb29
7 changed files with 107 additions and 31 deletions

View File

@@ -6,6 +6,7 @@ import { create } from "zustand"
import { fetchApi, useAuthenticatedQuery } from "~/api"
import { Button } from "~/components/button"
import { useDeleteBookmark } from "~/bookmark/api"
import { LoadingSpinner } from "~/components/loading-spinner"
import { useMnemonics } from "~/hooks/use-mnemonics"
import { Dialog, DialogActionRow, DialogBody, DialogTitle } from "~/components/dialog"
@@ -171,8 +172,8 @@ function DeleteBookmarkDialog() {
useMnemonics(
{
p: proceed,
c: cancel,
y: proceed,
n: cancel,
},
{ active: true },
)
@@ -192,22 +193,50 @@ function DeleteBookmarkDialog() {
markBookmarkForDeletion(null)
}
function body() {
switch (deleteBookmarkMutation.status) {
case "pending":
return (
<p>
Deleting <LoadingSpinner />
</p>
)
case "idle":
return (
<p>
The bookmark titled{" "}
<strong>
<em>"{bookmark.title}"</em>
</strong>{" "}
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>CONFIRM</DialogTitle>
<DialogBody>
The bookmark titled{" "}
<strong>
<em>"{bookmark.title}"</em>
</strong>{" "}
will be deleted. Proceed?
</DialogBody>
<DialogTitle>{title()}</DialogTitle>
<DialogBody>{body()}</DialogBody>
<DialogActionRow>
<Button disabled={deleteBookmarkMutation.isPending} onClick={proceed}>
<span className="underline">P</span>roceed
{deleteBookmarkMutation.isError ? "Retry" : "Proceed"} (y)
</Button>
<Button disabled={deleteBookmarkMutation.isPending} onClick={cancel}>
<span className="underline">C</span>ancel
Cancel (n)
</Button>
</DialogActionRow>
</Dialog>
@@ -215,17 +244,23 @@ function DeleteBookmarkDialog() {
}
function BookmarkListSection() {
const {
data: bookmarks,
error,
status,
} = useAuthenticatedQuery(["bookmarks"], () => fetchApi<LinkBookmark[]>("/bookmarks").then(([data]) => data))
const { data: bookmarks, status } = useAuthenticatedQuery(["bookmarks"], () =>
fetchApi<LinkBookmark[]>("/bookmarks").then(([data]) => data),
)
switch (status) {
case "pending":
return <p>Loading...</p>
return (
<p className="container max-w-2xl">
Loading&nbsp;
<LoadingSpinner />
</p>
)
case "success":
return <BookmarkList bookmarks={bookmarks} />
if (bookmarks) {
return <BookmarkList bookmarks={bookmarks} />
}
return null
default:
return null
}

View File

@@ -66,7 +66,7 @@ function Page() {
<form onSubmit={submitLoginForm}>
<FormField type="text" name="username" label="USERNAME" className="mb-2" />
<FormField type="password" name="password" label="PASSWORD" className="mb-8" />
<Button className="w-full py-2" type="submit">
<Button className="w-full py-2 md:py-2" type="submit">
Log in
</Button>
</form>

View File

@@ -9,9 +9,11 @@ function useDeleteBookmark() {
return useMutation({
mutationFn: ({ bookmark }: { bookmark: Bookmark }) =>
fetchApi(`/bookmark/${bookmark.id}`, {
method: "DELETE",
}),
new Promise((resolve) => setTimeout(resolve, 5000)).then(() =>
fetchApi(`/bookmark/${bookmark.id}`, {
method: "DELETE",
}),
),
onError: (error) => {
if (error instanceof UnauthenticatedError) {
navigate({ to: "/login", replace: true })

View File

@@ -0,0 +1,24 @@
import { useState, useEffect, useRef } from "react"
// courtesy of https://github.com/sindresorhus/cli-spinners
const FRAMES = ["-", "\\", "|", "/"]
function LoadingSpinner() {
const [frame, setFrame] = useState(0)
const timer = useRef<ReturnType<typeof setInterval>>(null)
useEffect(() => {
timer.current = setInterval(() => {
setFrame((frame) => (frame === 3 ? 0 : frame + 1))
}, 100)
return () => {
if (timer.current) {
clearInterval(timer.current)
}
}
}, [])
return <span className="whitespace-pre">{FRAMES[frame]}</span>
}
export { LoadingSpinner }

View File

@@ -12,8 +12,8 @@ body {
background: repeating-linear-gradient(
-45deg,
var(--color-stone-400),
var(--color-stone-400) 4px,
var(--color-stone-300) 4px,
var(--color-stone-300) 12px
var(--color-stone-400) 2px,
var(--color-stone-300) 2px,
var(--color-stone-300) 6px
);
}