A very simple bookmarking webapp bookmarker.finxol.deno.net/

feat: add logic and styles

finxol.io 63c04e93 4d1711ca

verified
+213 -17
+84 -3
src/components/BookmarkEditModal.css
··· 4 4 margin: auto; 5 5 width: min(90vw, 32rem); 6 6 max-height: min(80vh, 600px); 7 - padding: 1.5rem; 7 + padding: calc(var(--spacing) * 3); 8 8 border: none; 9 - border-radius: 0.75rem; 9 + border-radius: var(--radius); 10 10 background: var(--page-bg); 11 11 color: var(--primary-text); 12 12 flex-direction: column; 13 - gap: 1rem; 13 + gap: calc(var(--spacing) * 1); 14 14 box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); 15 15 16 16 &:popover-open { ··· 21 21 background: rgba(0, 0, 0, 0.5); 22 22 backdrop-filter: blur(4px); 23 23 } 24 + 25 + & > .button-ghost { 26 + align-self: flex-end; 27 + } 28 + 29 + & > div.title { 30 + display: flex; 31 + align-items: center; 32 + justify-content: space-between; 33 + margin-bottom: calc(var(--spacing) * 0.5); 34 + 35 + h2 { 36 + font-size: 1.25rem; 37 + font-weight: 700; 38 + } 39 + } 40 + } 41 + 42 + .edit-form { 43 + display: flex; 44 + flex-direction: column; 45 + gap: calc(var(--spacing) * 2); 46 + 47 + .input-group { 48 + display: flex; 49 + flex-direction: column; 50 + gap: calc(var(--spacing) * 0.5); 51 + 52 + label { 53 + font-size: 0.8rem; 54 + font-weight: 600; 55 + color: oklch(from var(--primary-text) l c h / 0.7); 56 + } 57 + 58 + input, 59 + textarea { 60 + width: 100%; 61 + padding: calc(var(--spacing) * 1); 62 + border: 1px solid oklch(from var(--primary-text) l c h / 0.15); 63 + border-radius: var(--radius); 64 + font-size: 0.9rem; 65 + font-family: inherit; 66 + color: var(--primary-text); 67 + background: transparent; 68 + outline: none; 69 + transition: all 0.15s ease; 70 + 71 + &::placeholder { 72 + color: oklch(from var(--primary-text) l c h / 0.3); 73 + } 74 + 75 + &:focus { 76 + border-color: var(--primary); 77 + box-shadow: 0 0 0 3px oklch(from var(--primary) l c h / 0.12); 78 + } 79 + 80 + &:disabled { 81 + opacity: 0.6; 82 + cursor: not-allowed; 83 + } 84 + } 85 + 86 + textarea { 87 + resize: vertical; 88 + min-height: 5rem; 89 + } 90 + } 91 + 92 + button[type="submit"] { 93 + align-self: flex-end; 94 + } 95 + } 96 + 97 + .bookmark-edit-modal .error-message { 98 + margin-top: calc(var(--spacing) * 0.5); 99 + padding: calc(var(--spacing) * 1); 100 + background: oklch(0.65 0.25 25 / 0.08); 101 + border: 1px solid oklch(0.65 0.25 25 / 0.2); 102 + border-radius: var(--radius); 103 + color: oklch(0.5 0.2 25); 104 + font-size: 0.85rem; 24 105 }
+125 -13
src/components/BookmarkEditModal.tsx
··· 1 - import { PencilIcon, XIcon } from "lucide-solid" 1 + import { LoaderIcon, PencilIcon, XIcon } from "lucide-solid" 2 + import { useMutation, useQueryClient } from "@tanstack/solid-query" 3 + import { createSignal, Show } from "solid-js" 4 + import { client } from "../apiclient.ts" 5 + import { Bookmark } from "../../server/utils/bookmarks.ts" 2 6 import "./BookmarkEditModal.css" 7 + import "./ui/button.css" 8 + 9 + interface BookmarkEditModalProps { 10 + bookmark: Bookmark & { id: string } 11 + } 12 + 13 + export function BookmarkEditModal(props: BookmarkEditModalProps) { 14 + const queryClient = useQueryClient() 15 + const [title, setTitle] = createSignal(props.bookmark.title) 16 + const [description, setDescription] = createSignal( 17 + props.bookmark.description, 18 + ) 3 19 4 - export function BookmarkEditModal(props: { id: string }) { 20 + const editBookmark = useMutation(() => ({ 21 + mutationFn: async () => { 22 + const res = await client.api.v1.bookmarks[":id"].$put({ 23 + param: { id: props.bookmark.id }, 24 + json: { 25 + title: title().trim(), 26 + description: description().trim(), 27 + }, 28 + }) 29 + if (!res.ok) { 30 + const data = await res.json() 31 + throw new Error( 32 + "error" in data ? data.error : "Failed to update bookmark", 33 + ) 34 + } 35 + return await res.json() 36 + }, 37 + onSuccess: async () => { 38 + await queryClient.invalidateQueries({ 39 + queryKey: [client.api.v1.bookmarks.all.$url().pathname], 40 + }) 41 + // Close the modal 42 + const dialog = document.getElementById( 43 + `edit-${props.bookmark.id}`, 44 + ) as HTMLDialogElement | null 45 + dialog?.requestClose() 46 + }, 47 + })) 48 + 49 + const handleSubmit = (e: SubmitEvent) => { 50 + e.preventDefault() 51 + const titleValue = title().trim() 52 + if (titleValue) { 53 + editBookmark.mutate() 54 + } 55 + } 56 + 5 57 return ( 6 58 <> 7 59 <button 8 60 type="button" 9 61 command="show-modal" 10 - commandfor={`edit-${props.id}`} 62 + commandfor={`edit-${props.bookmark.id}`} 11 63 class="bookmark-edit button-icon button-ghost" 12 64 > 13 65 <PencilIcon size={16} /> 14 66 </button> 15 - <dialog id={`edit-${props.id}`} class="bookmark-edit-modal"> 16 - <button 17 - type="button" 18 - commandfor={`edit-${props.id}`} 19 - command="close" 20 - class="button-ghost button-icon" 21 - > 22 - <XIcon size={16} /> 23 - </button> 24 - Edit 67 + <dialog 68 + id={`edit-${props.bookmark.id}`} 69 + class="bookmark-edit-modal" 70 + > 71 + <div class="title"> 72 + <h2>Edit bookmark</h2> 73 + 74 + <button 75 + type="button" 76 + commandfor={`edit-${props.bookmark.id}`} 77 + command="close" 78 + class="button-ghost button-icon" 79 + > 80 + <XIcon size={16} /> 81 + </button> 82 + </div> 83 + 84 + <form onSubmit={handleSubmit} class="edit-form"> 85 + <div class="input-group"> 86 + <label for={`title-${props.bookmark.id}`}>Title</label> 87 + <input 88 + id={`title-${props.bookmark.id}`} 89 + type="text" 90 + required 91 + value={title()} 92 + onInput={(e) => setTitle(e.currentTarget.value)} 93 + disabled={editBookmark.isPending} 94 + maxlength="200" 95 + /> 96 + </div> 97 + 98 + <div class="input-group"> 99 + <label for={`description-${props.bookmark.id}`}> 100 + Description 101 + </label> 102 + <textarea 103 + id={`description-${props.bookmark.id}`} 104 + value={description()} 105 + onInput={(e) => 106 + setDescription(e.currentTarget.value)} 107 + disabled={editBookmark.isPending} 108 + maxlength="500" 109 + /> 110 + </div> 111 + 112 + <button 113 + type="submit" 114 + class="button" 115 + disabled={editBookmark.isPending || !title()?.trim()} 116 + > 117 + <Show 118 + when={!editBookmark.isPending} 119 + fallback={ 120 + <> 121 + <LoaderIcon size={16} class="spinner" /> 122 + Saving... 123 + </> 124 + } 125 + > 126 + Save changes 127 + </Show> 128 + </button> 129 + </form> 130 + 131 + <Show when={editBookmark.isError}> 132 + <div class="error-message"> 133 + {editBookmark.error?.message ?? 134 + "Failed to update bookmark"} 135 + </div> 136 + </Show> 25 137 </dialog> 26 138 </> 27 139 )
+4 -1
src/routes/index.tsx
··· 155 155 <ExternalLinkIcon size={16} /> 156 156 </a> 157 157 <BookmarkEditModal 158 - id={String(bookmark.id)} 158 + bookmark={{ 159 + ...bookmark, 160 + id: String(bookmark.id), 161 + }} 159 162 /> 160 163 <button 161 164 type="button"