import type { SubmitErrorHandler, SubmitHandler } from "react-hook-form"; import React, { useEffect, useImperativeHandle, useRef } from "react"; import { ActionButton } from "@/components/ui/action-button"; import { Form, FormControl, FormItem } from "@/components/ui/form"; import InfoTooltip from "@/components/ui/info-tooltip"; import MultipleChoiceDialog from "@/components/ui/multiple-choice-dialog"; import { Separator } from "@/components/ui/separator"; import { Textarea } from "@/components/ui/textarea"; import { toast } from "@/components/ui/use-toast"; import BookmarkAlreadyExistsToast from "@/components/utils/BookmarkAlreadyExistsToast"; import { useClientConfig } from "@/lib/clientConfig"; import { useBookmarkLayout, useBookmarkLayoutSwitch, } from "@/lib/userLocalSettings/bookmarksLayout"; import { cn, getOS } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { useCreateBookmarkWithPostHook } from "@hoarder/shared-react/hooks/bookmarks"; import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; import { useUploadAsset } from "../UploadDropzone"; function useFocusOnKeyPress(inputRef: React.RefObject) { useEffect(() => { function handleKeyPress(e: KeyboardEvent) { if (!inputRef.current) { return; } if ((e.metaKey || e.ctrlKey) && e.code === "KeyE") { inputRef.current.focus(); e.preventDefault(); } } document.addEventListener("keydown", handleKeyPress); return () => { document.removeEventListener("keydown", handleKeyPress); }; }, [inputRef]); } interface MultiUrlImportState { urls: URL[]; text: string; } export default function EditorCard({ className }: { className?: string }) { const inputRef = useRef(null); const [multiUrlImportState, setMultiUrlImportState] = React.useState(null); const demoMode = !!useClientConfig().demoMode; const bookmarkLayout = useBookmarkLayout(); const formSchema = z.object({ text: z.string(), }); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { text: "", }, }); const { ref, ...textFieldProps } = form.register("text"); useImperativeHandle(ref, () => inputRef.current); useFocusOnKeyPress(inputRef); const { mutate, isPending } = useCreateBookmarkWithPostHook({ onSuccess: (resp) => { if (resp.alreadyExists) { toast({ description: , variant: "default", }); } form.reset(); // if the list layout is used, we reset the size of the editor card to the original size after submitting if (bookmarkLayout === "list" && inputRef?.current?.style) { inputRef.current.style.height = "auto"; } }, onError: (e) => { toast({ description: e.message, variant: "destructive" }); }, }); const uploadAsset = useUploadAsset(); function tryToImportUrls(text: string): void { const lines = text.split("\n"); const urls: URL[] = []; for (const line of lines) { // parsing can also throw an exception, but will be caught outside const url = new URL(line); if (url.protocol != "http:" && url.protocol != "https:") { throw new Error("Invalid URL"); } urls.push(url); } if (urls.length === 1) { // Only 1 url in the textfield --> simply import it mutate({ type: BookmarkTypes.LINK, url: text }); return; } // multiple urls found --> ask the user if it should be imported as multiple URLs or as a text bookmark setMultiUrlImportState({ urls, text }); } const onInput = (e: React.FormEvent) => { // Expand the textarea to a max of half the screen size in the list layout only if (bookmarkLayout === "list") { const target = e.target as HTMLTextAreaElement; const maxHeight = window.innerHeight * 0.5; target.style.height = "auto"; if (target.scrollHeight <= maxHeight) { target.style.height = `${target.scrollHeight}px`; } else { target.style.height = `${maxHeight}px`; } } }; const onSubmit: SubmitHandler> = (data) => { const text = data.text.trim(); if (!text.length) return; try { tryToImportUrls(text); } catch (e) { // Not a URL mutate({ type: BookmarkTypes.TEXT, text }); } }; const onError: SubmitErrorHandler> = (errors) => { toast({ description: Object.values(errors) .map((v) => v.message) .join("\n"), variant: "destructive", }); }; const cardHeight = useBookmarkLayoutSwitch({ grid: "h-96", masonry: "h-96", list: undefined, compact: undefined, }); const handlePaste = async ( event: React.ClipboardEvent, ) => { if (event?.clipboardData?.items) { await Promise.all( Array.from(event.clipboardData.items) .filter((item) => item?.type?.startsWith("image")) .map((item) => { const blob = item.getAsFile(); if (blob) { return uploadAsset(blob); } }), ); } }; const OS = getOS(); return (

NEW ITEM

You can quickly focus on this field by pressing ⌘ + E