wip: implement bookmark preview
This commit is contained in:
@@ -22,18 +22,14 @@ VALUES ($id, $userId, $kind, $title, $url)
|
|||||||
insert(DEMO_BOOKMARKS)
|
insert(DEMO_BOOKMARKS)
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertBookmark(bookmark: Bookmark, user: User) {
|
function findBookmarkHtml(id: string, user: User): string | null {
|
||||||
const query = db.query(`
|
const query = db.query("SELECT content_html FROM bookmarks WHERE id = $id AND user_id = $userId")
|
||||||
INSERT INTO bookmarks (id, user_id, kind, title, url)
|
const row = query.get({ id, userId: user.id })
|
||||||
VALUES ($id, $userId, $kind, $title, $url)
|
if (!row) {
|
||||||
`)
|
return null
|
||||||
query.run({
|
}
|
||||||
id: bookmark.id,
|
const { content_html } = row as { content_html: string }
|
||||||
userId: user.id,
|
return content_html
|
||||||
kind: bookmark.kind,
|
|
||||||
title: bookmark.title,
|
|
||||||
url: bookmark.url,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { insertDemoBookmarks }
|
export { insertDemoBookmarks, findBookmarkHtml }
|
||||||
|
@@ -7,12 +7,13 @@ import { HttpError } from "~/error.ts"
|
|||||||
import type { User } from "~/user/user.ts"
|
import type { User } from "~/user/user.ts"
|
||||||
import { JSDOM } from "jsdom"
|
import { JSDOM } from "jsdom"
|
||||||
import { Readability } from "@mozilla/readability"
|
import { Readability } from "@mozilla/readability"
|
||||||
|
import { findBookmarkHtml } from "./bookmark.ts"
|
||||||
|
|
||||||
const BOOKMARK_PAGINATION_LIMIT = 100
|
const BOOKMARK_PAGINATION_LIMIT = 100
|
||||||
|
|
||||||
const ListUserBookmarksParams = type({
|
const ListUserBookmarksParams = type({
|
||||||
limit: ["number", "=", BOOKMARK_PAGINATION_LIMIT],
|
limit: ["number", "=", BOOKMARK_PAGINATION_LIMIT],
|
||||||
skip: ["number", "=", 5],
|
skip: ["number", "=", 0],
|
||||||
})
|
})
|
||||||
|
|
||||||
const AddBookmarkRequestBody = type({
|
const AddBookmarkRequestBody = type({
|
||||||
@@ -31,11 +32,10 @@ async function listUserBookmarks(request: Bun.BunRequest<"/api/bookmarks">, user
|
|||||||
|
|
||||||
const listBookmarksQuery = db.query(
|
const listBookmarksQuery = db.query(
|
||||||
`
|
`
|
||||||
SELECT bookmarks.id, bookmarks.kind, bookmarks.title, bookmarks.url, tags.name as tag FROM bookmarks
|
SELECT bookmarks.id, bookmarks.kind, bookmarks.title, bookmarks.url FROM bookmarks
|
||||||
LEFT JOIN tags
|
|
||||||
ON bookmarks.id = tags.bookmark_id
|
|
||||||
WHERE bookmarks.user_id = $userId
|
WHERE bookmarks.user_id = $userId
|
||||||
ORDER BY bookmarks.id LIMIT $limit OFFSET $skip
|
ORDER BY bookmarks.id DESC
|
||||||
|
LIMIT $limit OFFSET $skip
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,26 +67,47 @@ async function addBookmark(request: Bun.BunRequest<"/api/bookmarks">, user: User
|
|||||||
}
|
}
|
||||||
|
|
||||||
const websiteResponse = await fetch(body.url).catch(() => {
|
const websiteResponse = await fetch(body.url).catch(() => {
|
||||||
|
if (body.force) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
throw new HttpError(400, "WebsiteUnreachable")
|
throw new HttpError(400, "WebsiteUnreachable")
|
||||||
})
|
})
|
||||||
const websiteText = await websiteResponse.text().catch(() => {
|
const websiteText = websiteResponse
|
||||||
throw new HttpError(400, "UnsupportedWebsite")
|
? await websiteResponse.text().catch(() => {
|
||||||
})
|
throw new HttpError(400, "UnsupportedWebsite")
|
||||||
|
})
|
||||||
const dom = new JSDOM(websiteText, {
|
: null
|
||||||
url: body.url,
|
|
||||||
})
|
|
||||||
const reader = new Readability(dom.window.document)
|
|
||||||
const article = reader.parse()
|
|
||||||
|
|
||||||
const bookmark: LinkBookmark = {
|
const bookmark: LinkBookmark = {
|
||||||
kind: "link",
|
kind: "link",
|
||||||
id: ulid(),
|
id: ulid(),
|
||||||
title: body.title || article?.title || "Untitled",
|
title: "",
|
||||||
url: body.url,
|
url: body.url,
|
||||||
tags: body.tags.map((tag) => ({ id: ulid(), name: tag })),
|
tags: body.tags.map((tag) => ({ id: ulid(), name: tag })),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (body.title) {
|
||||||
|
bookmark.title = body.title
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentHtml: string
|
||||||
|
if (websiteText) {
|
||||||
|
const dom = new JSDOM(websiteText, {
|
||||||
|
url: body.url,
|
||||||
|
})
|
||||||
|
const reader = new Readability(dom.window.document)
|
||||||
|
const article = reader.parse()
|
||||||
|
if (!bookmark.title) {
|
||||||
|
bookmark.title = article?.title || "Untitled"
|
||||||
|
}
|
||||||
|
contentHtml = article?.content || ""
|
||||||
|
} else {
|
||||||
|
contentHtml = ""
|
||||||
|
if (!bookmark.title) {
|
||||||
|
bookmark.title = "Untitled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const query = db.query(`
|
const query = db.query(`
|
||||||
INSERT INTO bookmarks (id, user_id, kind, title, url, content_html)
|
INSERT INTO bookmarks (id, user_id, kind, title, url, content_html)
|
||||||
VALUES ($id, $userId, $kind, $title, $url, $html)
|
VALUES ($id, $userId, $kind, $title, $url, $html)
|
||||||
@@ -97,7 +118,7 @@ VALUES ($id, $userId, $kind, $title, $url, $html)
|
|||||||
kind: bookmark.kind,
|
kind: bookmark.kind,
|
||||||
title: bookmark.title,
|
title: bookmark.title,
|
||||||
url: bookmark.url,
|
url: bookmark.url,
|
||||||
html: article?.content ?? websiteText,
|
html: contentHtml,
|
||||||
})
|
})
|
||||||
|
|
||||||
const insertTagQuery = db.query(`
|
const insertTagQuery = db.query(`
|
||||||
@@ -122,4 +143,23 @@ VALUES ($id, $bookmarkId, $name)
|
|||||||
return Response.json(undefined, { status: 204 })
|
return Response.json(undefined, { status: 204 })
|
||||||
}
|
}
|
||||||
|
|
||||||
export { addBookmark, listUserBookmarks, deleteUserBookmark }
|
async function fetchBookmark(request: Bun.BunRequest<"/api/bookmark/:id">, user: User) {
|
||||||
|
switch (request.headers.get("Accept")) {
|
||||||
|
case "text/html": {
|
||||||
|
const html = findBookmarkHtml(request.params.id, user)
|
||||||
|
if (html === null) {
|
||||||
|
throw new HttpError(404)
|
||||||
|
}
|
||||||
|
return new Response(html, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/html",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new HttpError(400, "UnsupportedAcceptHeader")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { addBookmark, fetchBookmark, listUserBookmarks, deleteUserBookmark }
|
||||||
|
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS bookmarks(
|
|||||||
kind TEXT NOT NULL,
|
kind TEXT NOT NULL,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
url TEXT NOT NULL,
|
url TEXT NOT NULL,
|
||||||
content_html TEXT NOT NULL,
|
content_html TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tags(
|
CREATE TABLE IF NOT EXISTS tags(
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { authenticated, login, logout, signUp } from "./auth/auth.ts"
|
import { authenticated, login, logout, signUp } from "./auth/auth.ts"
|
||||||
import { startBackgroundSessionCleanup } from "./auth/session.ts"
|
import { startBackgroundSessionCleanup } from "./auth/session.ts"
|
||||||
import { insertDemoBookmarks } from "./bookmark/bookmark.ts"
|
import { insertDemoBookmarks } from "./bookmark/bookmark.ts"
|
||||||
import { addBookmark, listUserBookmarks, deleteUserBookmark } from "./bookmark/handlers.ts"
|
import { addBookmark, listUserBookmarks, deleteUserBookmark, fetchBookmark } from "./bookmark/handlers.ts"
|
||||||
import { migrateDatabase } from "./database.ts"
|
import { migrateDatabase } from "./database.ts"
|
||||||
import { httpHandler, preflightHandler } from "./http-handler.ts"
|
import { httpHandler, preflightHandler } from "./http-handler.ts"
|
||||||
import { createDemoUser } from "./user/user.ts"
|
import { createDemoUser } from "./user/user.ts"
|
||||||
@@ -28,6 +28,7 @@ async function main() {
|
|||||||
POST: authenticated(addBookmark),
|
POST: authenticated(addBookmark),
|
||||||
},
|
},
|
||||||
"/api/bookmark/:id": {
|
"/api/bookmark/:id": {
|
||||||
|
GET: authenticated(fetchBookmark),
|
||||||
DELETE: authenticated(deleteUserBookmark),
|
DELETE: authenticated(deleteUserBookmark),
|
||||||
OPTIONS: preflightHandler({
|
OPTIONS: preflightHandler({
|
||||||
allowedMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
allowedMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
||||||
|
@@ -24,7 +24,7 @@ interface ErrorBody {
|
|||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryKey = ["bookmarks", ...ReadonlyArray<unknown>]
|
type QueryKey = ["bookmarks", ...ReadonlyArray<unknown>] | ["bookmarks", string, ...ReadonlyArray<unknown>]
|
||||||
|
|
||||||
async function fetchApi(route: string, init?: RequestInit): Promise<Response> {
|
async function fetchApi(route: string, init?: RequestInit): Promise<Response> {
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/api${route}`, {
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/api${route}`, {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import type { LinkBookmark } from "@markone/core/bookmark"
|
import type { LinkBookmark } from "@markone/core/bookmark"
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import { useEffect, useId, useState } from "react"
|
import { useCallback, useEffect, useId, useRef, useState } from "react"
|
||||||
import { create } from "zustand"
|
import { create } from "zustand"
|
||||||
import { fetchApi, useAuthenticatedQuery, BadRequestError, ApiErrorCode } from "~/api"
|
import { fetchApi, useAuthenticatedQuery, BadRequestError, ApiErrorCode } from "~/api"
|
||||||
import { useCreateBookmark, useDeleteBookmark } from "~/bookmark/api"
|
import { useCreateBookmark, useDeleteBookmark } from "~/bookmark/api"
|
||||||
@@ -131,8 +131,8 @@ function Page() {
|
|||||||
<BookmarkListSection />
|
<BookmarkListSection />
|
||||||
</div>
|
</div>
|
||||||
<BookmarkPreview />
|
<BookmarkPreview />
|
||||||
|
<ActionBar />
|
||||||
</Main>
|
</Main>
|
||||||
<ActionBar />
|
|
||||||
<PageDialog />
|
<PageDialog />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -178,7 +178,7 @@ function DeleteBookmarkDialog() {
|
|||||||
y: proceed,
|
y: proceed,
|
||||||
n: cancel,
|
n: cancel,
|
||||||
},
|
},
|
||||||
{ active: true },
|
{ ignore: () => false },
|
||||||
)
|
)
|
||||||
|
|
||||||
async function proceed() {
|
async function proceed() {
|
||||||
@@ -207,10 +207,14 @@ function DeleteBookmarkDialog() {
|
|||||||
case "idle":
|
case "idle":
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
The bookmark titled{" "}
|
The bookmark titled:
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
<strong>
|
<strong>
|
||||||
<em>"{bookmark.title}"</em>
|
<em>"{bookmark.title}"</em>
|
||||||
</strong>{" "}
|
</strong>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
will be deleted. Proceed?
|
will be deleted. Proceed?
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
@@ -251,14 +255,30 @@ function AddBookmarkDialog() {
|
|||||||
const createBookmarkMutation = useCreateBookmark()
|
const createBookmarkMutation = useCreateBookmark()
|
||||||
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
|
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
|
||||||
const formId = useId()
|
const formId = useId()
|
||||||
|
const linkInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
useMnemonics(
|
useMnemonics(
|
||||||
{
|
{
|
||||||
c: cancel,
|
c: () => {
|
||||||
|
if (linkInputRef.current !== document.activeElement) {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Escape: () => {
|
||||||
|
linkInputRef.current?.blur()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ active: true },
|
{ ignore: () => false },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (linkInputRef.current) {
|
||||||
|
linkInputRef.current.focus()
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}, [])
|
||||||
|
|
||||||
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
|
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
@@ -267,9 +287,12 @@ function AddBookmarkDialog() {
|
|||||||
if (url && typeof url === "string") {
|
if (url && typeof url === "string") {
|
||||||
try {
|
try {
|
||||||
await createBookmarkMutation.mutateAsync({ url, force: isWebsiteUnreachable })
|
await createBookmarkMutation.mutateAsync({ url, force: isWebsiteUnreachable })
|
||||||
|
setActiveDialog(ActiveDialog.None)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof BadRequestError && error.code === ApiErrorCode.WebsiteUnreachable) {
|
if (error instanceof BadRequestError && error.code === ApiErrorCode.WebsiteUnreachable) {
|
||||||
setIsWebsiteUnreachable(true)
|
setIsWebsiteUnreachable(true)
|
||||||
|
} else {
|
||||||
|
setIsWebsiteUnreachable(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,6 +334,7 @@ function AddBookmarkDialog() {
|
|||||||
{message()}
|
{message()}
|
||||||
<form id={formId} className="px-8" onSubmit={onSubmit}>
|
<form id={formId} className="px-8" onSubmit={onSubmit}>
|
||||||
<FormField
|
<FormField
|
||||||
|
ref={linkInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
name="link"
|
name="link"
|
||||||
label="LINK"
|
label="LINK"
|
||||||
@@ -426,11 +450,36 @@ function BookmarkPreview() {
|
|||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p>Content here</p>
|
<BookmarkPreviewFrame />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BookmarkPreviewFrame() {
|
||||||
|
const selectedBookmarkId = useBookmarkPageStore((state) => state.selectedBookmarkId)
|
||||||
|
const { data, status } = useAuthenticatedQuery(["bookmarks", selectedBookmarkId], () =>
|
||||||
|
fetchApi(`/bookmark/${selectedBookmarkId}`, {
|
||||||
|
headers: {
|
||||||
|
Accept: "text/html",
|
||||||
|
},
|
||||||
|
}).then((res) => res.text()),
|
||||||
|
)
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "pending":
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Loading <LoadingSpinner />
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
case "success":
|
||||||
|
return <iframe className="bg-stone-200 dark:bg-stone-900 w-full h-full pb-20" srcDoc={data} />
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function BookmarkListItem({ bookmark, index }: { bookmark: LinkBookmark; index: number }) {
|
function BookmarkListItem({ bookmark, index }: { bookmark: LinkBookmark; index: number }) {
|
||||||
const url = new URL(bookmark.url)
|
const url = new URL(bookmark.url)
|
||||||
const selectedBookmarkIndex = useBookmarkPageStore((state) => state.selectedBookmarkIndex)
|
const selectedBookmarkIndex = useBookmarkPageStore((state) => state.selectedBookmarkIndex)
|
||||||
@@ -446,7 +495,12 @@ function BookmarkListItem({ bookmark, index }: { bookmark: LinkBookmark; index:
|
|||||||
{
|
{
|
||||||
d: deleteItem,
|
d: deleteItem,
|
||||||
},
|
},
|
||||||
{ active: isSelected },
|
{
|
||||||
|
ignore: useCallback(
|
||||||
|
() => !isSelected || useBookmarkPageStore.getState().activeDialog !== ActiveDialog.None,
|
||||||
|
[isSelected],
|
||||||
|
),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
function deleteItem() {
|
function deleteItem() {
|
||||||
@@ -499,7 +553,7 @@ function BookmarkListItem({ bookmark, index }: { bookmark: LinkBookmark; index:
|
|||||||
<p className="opacity-80 text-sm">{url.host}</p>
|
<p className="opacity-80 text-sm">{url.host}</p>
|
||||||
{isBookmarkItemExpanded && isSelected ? (
|
{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">
|
<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 #devops #devops #devops #devops #devops #devops</p>
|
<p className="text-sm">#dev</p>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<OpenBookmarkPreviewButton />
|
<OpenBookmarkPreviewButton />
|
||||||
<Button variant="light" className="text-sm">
|
<Button variant="light" className="text-sm">
|
||||||
@@ -579,7 +633,7 @@ function ActionBar() {
|
|||||||
{
|
{
|
||||||
a: addBookmark,
|
a: addBookmark,
|
||||||
},
|
},
|
||||||
{ active: true },
|
{ ignore: useCallback(() => useBookmarkPageStore.getState().activeDialog !== ActiveDialog.None, []) },
|
||||||
)
|
)
|
||||||
|
|
||||||
function addBookmark() {
|
function addBookmark() {
|
||||||
@@ -587,7 +641,7 @@ function ActionBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed z-10 bottom-0 left-0 right-0 border-t-1 flex flex-row justify-center py-4 space-x-4">
|
<div 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">
|
||||||
<Button onClick={addBookmark}>
|
<Button onClick={addBookmark}>
|
||||||
<span className="underline">A</span>DD
|
<span className="underline">A</span>DD
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -9,11 +9,9 @@ function useDeleteBookmark() {
|
|||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ bookmark }: { bookmark: Bookmark }) =>
|
mutationFn: ({ bookmark }: { bookmark: Bookmark }) =>
|
||||||
new Promise((resolve) => setTimeout(resolve, 5000)).then(() =>
|
fetchApi(`/bookmark/${bookmark.id}`, {
|
||||||
fetchApi(`/bookmark/${bookmark.id}`, {
|
method: "DELETE",
|
||||||
method: "DELETE",
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (error instanceof UnauthenticatedError) {
|
if (error instanceof UnauthenticatedError) {
|
||||||
navigate({ to: "/login", replace: true })
|
navigate({ to: "/login", replace: true })
|
||||||
|
@@ -17,7 +17,7 @@ function DialogTitle({ children }: React.PropsWithChildren) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DialogBody({ children }: React.PropsWithChildren) {
|
function DialogBody({ children }: React.PropsWithChildren) {
|
||||||
return <div className="m-8 w-full text-center">{children}</div>
|
return <div className="p-8 w-full text-center">{children}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogActionRow({ children }: React.PropsWithChildren) {
|
function DialogActionRow({ children }: React.PropsWithChildren) {
|
||||||
|
@@ -9,17 +9,31 @@ interface FormFieldProps {
|
|||||||
className?: string
|
className?: string
|
||||||
labelClassName?: string
|
labelClassName?: string
|
||||||
required?: boolean
|
required?: boolean
|
||||||
|
autoFocus?: boolean
|
||||||
|
ref?: React.Ref<HTMLInputElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormField({ name, label, type, className, labelClassName, required = false }: FormFieldProps) {
|
function FormField({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
type,
|
||||||
|
className,
|
||||||
|
labelClassName,
|
||||||
|
required = false,
|
||||||
|
autoFocus = false,
|
||||||
|
ref,
|
||||||
|
}: FormFieldProps) {
|
||||||
const id = useId()
|
const id = useId()
|
||||||
return (
|
return (
|
||||||
<div className={clsx("flex flex-col-reverse focus:text-teal-600", className)}>
|
<div className={clsx("flex flex-col-reverse focus:text-teal-600", className)}>
|
||||||
<input
|
<input
|
||||||
|
ref={ref}
|
||||||
id={id}
|
id={id}
|
||||||
required={required}
|
required={required}
|
||||||
name={name}
|
name={name}
|
||||||
type={type}
|
type={type}
|
||||||
|
// biome-ignore lint/a11y/noAutofocus: <explanation>
|
||||||
|
autoFocus={autoFocus}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
className="peer px-3 pb-2 pt-3 border focus:border-2 border-stone-800 dark:border-stone-200 focus:border-teal-600 focus:ring-0 focus:outline-none"
|
className="peer px-3 pb-2 pt-3 border focus:border-2 border-stone-800 dark:border-stone-200 focus:border-teal-600 focus:ring-0 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
|
@@ -1,17 +1,20 @@
|
|||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
|
|
||||||
function useMnemonics(mnemonicMap: Record<string, (event: KeyboardEvent) => void>, { active }: { active: boolean }) {
|
function useMnemonics(
|
||||||
|
mnemonicMap: Record<string, (event: KeyboardEvent) => void>,
|
||||||
|
{ ignore }: { ignore: (event: KeyboardEvent) => boolean },
|
||||||
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onKeyDown(event: KeyboardEvent) {
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
mnemonicMap[event.key]?.(event)
|
if (!ignore(event)) {
|
||||||
}
|
mnemonicMap[event.key]?.(event)
|
||||||
if (active) {
|
}
|
||||||
document.addEventListener("keydown", onKeyDown)
|
|
||||||
}
|
}
|
||||||
|
document.addEventListener("keydown", onKeyDown)
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", onKeyDown)
|
document.removeEventListener("keydown", onKeyDown)
|
||||||
}
|
}
|
||||||
}, [mnemonicMap, active])
|
}, [mnemonicMap, ignore])
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useMnemonics }
|
export { useMnemonics }
|
||||||
|
Reference in New Issue
Block a user