A very simple bookmarking webapp
bookmarker.finxol.deno.net/
1import { LoaderIcon, PencilIcon, XIcon } from "lucide-solid"
2import { useMutation, useQueryClient } from "@tanstack/solid-query"
3import { createSignal, Show } from "solid-js"
4import { client } from "../apiclient.ts"
5import { Bookmark } from "../../server/utils/bookmarks.ts"
6import "./BookmarkEditModal.css"
7import "./ui/button.css"
8import { useWebHaptics } from "../utils/haptics.ts"
9
10interface BookmarkEditModalProps {
11 bookmark: Bookmark & { id: string }
12}
13
14export function BookmarkEditModal(props: BookmarkEditModalProps) {
15 const haptics = useWebHaptics()
16
17 const queryClient = useQueryClient()
18 const [title, setTitle] = createSignal(props.bookmark.title)
19 const [description, setDescription] = createSignal(
20 props.bookmark.description,
21 )
22
23 const editBookmark = useMutation(() => ({
24 mutationFn: async () => {
25 const res = await client.api.v1.bookmarks[":id"].$put({
26 param: { id: props.bookmark.id },
27 json: {
28 title: title().trim(),
29 description: description().trim(),
30 },
31 })
32 if (!res.ok) {
33 const data = await res.json()
34 haptics.trigger([
35 { duration: 40, intensity: 0.7 },
36 { delay: 40, duration: 40, intensity: 0.7 },
37 { delay: 40, duration: 40, intensity: 0.9 },
38 { delay: 40, duration: 50, intensity: 0.6 },
39 ])
40 throw new Error(
41 "error" in data ? data.error : "Failed to update bookmark",
42 )
43 }
44 return await res.json()
45 },
46 onSuccess: async () => {
47 await queryClient.invalidateQueries({
48 queryKey: [client.api.v1.bookmarks.all.$url().pathname],
49 })
50 // Close the modal
51 const dialog = document.getElementById(
52 `edit-${props.bookmark.id}`,
53 ) as HTMLDialogElement | null
54 dialog?.requestClose()
55 },
56 }))
57
58 const handleSubmit = (e: SubmitEvent) => {
59 e.preventDefault()
60 const titleValue = title().trim()
61 if (titleValue) {
62 editBookmark.mutate()
63 }
64 }
65
66 return (
67 <>
68 <button
69 type="button"
70 command="show-modal"
71 commandfor={`edit-${props.bookmark.id}`}
72 class="bookmark-edit button-icon button-ghost"
73 onClick={() => haptics.trigger()}
74 >
75 <PencilIcon size={16} />
76 </button>
77 <dialog
78 id={`edit-${props.bookmark.id}`}
79 class="bookmark-edit-modal"
80 >
81 <div class="title">
82 <h2>Edit bookmark</h2>
83
84 <button
85 type="button"
86 commandfor={`edit-${props.bookmark.id}`}
87 command="close"
88 class="button-ghost button-icon"
89 >
90 <XIcon size={16} />
91 </button>
92 </div>
93
94 <form onSubmit={handleSubmit} class="edit-form">
95 <div class="input-group">
96 <label for={`title-${props.bookmark.id}`}>Title</label>
97 <input
98 id={`title-${props.bookmark.id}`}
99 type="text"
100 required
101 value={title()}
102 onInput={(e) => setTitle(e.currentTarget.value)}
103 disabled={editBookmark.isPending}
104 maxlength="200"
105 />
106 </div>
107
108 <div class="input-group">
109 <label for={`description-${props.bookmark.id}`}>
110 Description
111 </label>
112 <textarea
113 id={`description-${props.bookmark.id}`}
114 value={description()}
115 onInput={(e) =>
116 setDescription(e.currentTarget.value)}
117 disabled={editBookmark.isPending}
118 maxlength="500"
119 />
120 </div>
121
122 <button
123 type="submit"
124 class="button"
125 disabled={editBookmark.isPending || !title()?.trim()}
126 >
127 <Show
128 when={!editBookmark.isPending}
129 fallback={
130 <>
131 <LoaderIcon size={16} class="spinner" />
132 Saving...
133 </>
134 }
135 >
136 Save changes
137 </Show>
138 </button>
139 </form>
140
141 <Show when={editBookmark.isError}>
142 <div class="error-message">
143 {editBookmark.error?.message ??
144 "Failed to update bookmark"}
145 </div>
146 </Show>
147 </dialog>
148 </>
149 )
150}