"use client" import { useState, useEffect, useMemo } from "react" import { cn } from "@lib/utils" import { Button } from "@ui/components/button" import { dmSansClassName } from "@/lib/fonts" import { useHotkeys } from "react-hotkeys-hook" import { Image as ImageIcon, Loader2 } from "lucide-react" import { toast } from "sonner" import { extractUrls, getFaviconUrl, normalizeUrl } from "@/lib/url-helpers" export interface LinkData { url: string title: string description: string image?: string bulkUrls?: string[] } interface LinkContentProps { onSubmit?: (data: LinkData) => void onDataChange?: (data: LinkData) => void isSubmitting?: boolean isOpen?: boolean initialData?: LinkData } const prettyUrl = (url: string) => url.replace(/^https?:\/\//, "").replace(/\/$/, "") export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen, initialData, }: LinkContentProps) { const [url, setUrl] = useState(initialData?.url ?? "") const [title, setTitle] = useState(initialData?.title ?? "") const [description, setDescription] = useState(initialData?.description ?? "") const [image, setImage] = useState(initialData?.image) const [isPreviewLoading, setIsPreviewLoading] = useState(false) const [deselected, setDeselected] = useState>(new Set()) const { urls: detectedUrls, duplicates } = useMemo( () => extractUrls(url), [url], ) const isBulk = detectedUrls.length >= 2 const selectedUrls = useMemo( () => detectedUrls.filter((u) => !deselected.has(u)), [detectedUrls, deselected], ) const canSubmit = (isBulk ? selectedUrls.length > 0 : url.trim().length > 0) && !isSubmitting useEffect(() => { const selected = detectedUrls.filter((u) => !deselected.has(u)) onDataChange?.({ url: url.trim(), title, description, ...(image && { image }), ...(detectedUrls.length >= 2 && { bulkUrls: selected }), }) }, [url, title, description, image, detectedUrls, deselected, onDataChange]) const handleSubmit = () => { if (!canSubmit || !onSubmit) return if (isBulk) { onSubmit({ url: "", title, description, bulkUrls: selectedUrls }) return } onSubmit({ url: normalizeUrl(url.trim()), title, description }) } const toggleUrl = (u: string) => setDeselected((prev) => { const next = new Set(prev) if (next.has(u)) next.delete(u) else next.add(u) return next }) const handlePreviewLink = async () => { if (!url.trim()) { toast.error("Please enter a URL first") return } const normalizedUrl = normalizeUrl(url.trim()) if (normalizedUrl !== url) setUrl(normalizedUrl) setIsPreviewLoading(true) try { const response = await fetch( `/api/og?url=${encodeURIComponent(normalizedUrl)}`, ) if (!response.ok) { const errorData = await response.json().catch(() => ({})) throw new Error(errorData.error || "Failed to fetch preview") } const data = await response.json() setTitle(data.title || "") setDescription(data.description || "") setImage(data.image || undefined) if (!data.title && !data.description && !data.image) { toast.info("No Open Graph data found for this URL") } else { toast.success("Preview loaded successfully") } } catch (error) { console.error("Preview error:", error) toast.error( error instanceof Error ? error.message : "Failed to load preview", ) } finally { setIsPreviewLoading(false) } } useHotkeys("mod+enter", handleSubmit, { enabled: isOpen && canSubmit, enableOnFormTags: ["INPUT", "TEXTAREA"], }) useEffect(() => { if (!isOpen) { setUrl("") setTitle("") setDescription("") setImage(undefined) setDeselected(new Set()) onDataChange?.({ url: "", title: "", description: "" }) } }, [isOpen, onDataChange]) return (

Paste one link — or many — to turn them into memories