diff --git a/src/components/modals/CreateWebsiteDialog.tsx b/src/components/modals/CreateWebsiteDialog.tsx index 490d858f..f8eb93f5 100644 --- a/src/components/modals/CreateWebsiteDialog.tsx +++ b/src/components/modals/CreateWebsiteDialog.tsx @@ -10,17 +10,14 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; +import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { toast } from "sonner"; -import { Loader2 } from "lucide-react"; interface CreateWebsiteDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - workspaceId: string; - folderId?: string; - onNoteCreated?: (noteId: string) => void; + onCreate: (url: string, name: string, favicon?: string) => void; } /** @@ -35,225 +32,156 @@ function isValidUrl(str: string): boolean { } } +/** + * Get the favicon URL for a given website URL using Google's favicon service + * Note: We use sz=64 to detect default globe icons (they stay 16x16 while real favicons scale) + */ +function getFaviconUrl(websiteUrl: string): string | undefined { + try { + const url = new URL(websiteUrl.trim()); + return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=64`; + } catch { + return undefined; + } +} + +/** + * Extract a display name from a URL (domain without www) + */ +function getDisplayName(websiteUrl: string): string { + try { + const url = new URL(websiteUrl.trim()); + return url.hostname.replace(/^www\./, ""); + } catch { + return "Website"; + } +} + export function CreateWebsiteDialog({ open, onOpenChange, - workspaceId, - folderId, - onNoteCreated, + onCreate, }: CreateWebsiteDialogProps) { - const [urlsText, setUrlsText] = useState(""); - const [isCreating, setIsCreating] = useState(false); - const textareaRef = useRef(null); + const [url, setUrl] = useState(""); + const [name, setName] = useState(""); + const [isValid, setIsValid] = useState(false); + const [isLoadingTitle, setIsLoadingTitle] = useState(false); + const debounceTimerRef = useRef(null); - // Parse and validate URLs from textarea - const parseUrls = useCallback(() => { - const lines = urlsText.split("\n").map((line) => line.trim()).filter(Boolean); - const validUrls: string[] = []; - const invalidLines: string[] = []; + // Validate URL as user types + useEffect(() => { + setIsValid(isValidUrl(url)); + }, [url]); - for (const line of lines) { - if (isValidUrl(line)) { - validUrls.push(line); - } else { - invalidLines.push(line); - } + // Auto-fill name from domain when a valid URL is entered + useEffect(() => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); } - return { validUrls, invalidLines }; - }, [urlsText]); - - const { validUrls, invalidLines } = parseUrls(); - const hasValidUrls = validUrls.length > 0; - - const handleSubmit = useCallback(async () => { - if (!hasValidUrls || isCreating) return; - - if (invalidLines.length > 0) { - toast.warning(`Skipping ${invalidLines.length} invalid URL(s)`); + if (isValid && url.trim() && !name.trim()) { + debounceTimerRef.current = setTimeout(() => { + setName(getDisplayName(url)); + }, 300); } - // Close dialog immediately for non-blocking UX - onOpenChange(false); - setUrlsText(""); - - // Show loading toast - const toastId = toast.loading("Creating note from websites...", { - description: `Processing ${validUrls.length} URL${validUrls.length > 1 ? 's' : ''}`, - }); - - setIsCreating(true); - - try { - const response = await fetch("/api/notes/create-from-urls", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - urls: validUrls, - workspaceId, - folderId, - }), - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error || `Request failed with status ${response.status}`); + return () => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); } + }; + }, [url, isValid]); - const result = await response.json(); - - if (result.success && result.itemId) { - if (result.warning) { - toast.warning("Note created with issues", { - id: toastId, - description: result.warning, - }); - } else { - toast.success("Website note created!", { - id: toastId, - description: "Your note is ready", - }); - } - onNoteCreated?.(result.itemId); - } else { - throw new Error(result.message || "Failed to create note"); - } - } catch (error) { - console.error("Error creating website note:", error); - toast.error("Failed to create note from websites", { - id: toastId, - description: error instanceof Error ? error.message : undefined, - }); - } finally { - setIsCreating(false); + const handleSubmit = useCallback(() => { + if (!isValid || !url.trim()) { + toast.error("Please enter a valid website URL"); + return; } - }, [validUrls, hasValidUrls, isCreating, invalidLines.length, workspaceId, folderId, onNoteCreated, onOpenChange]); - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - // Cmd/Ctrl + Enter to submit - if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && hasValidUrls && !isCreating) { - e.preventDefault(); - handleSubmit(); - } else if (e.key === "Escape") { - onOpenChange(false); - } - }, - [hasValidUrls, isCreating, handleSubmit, onOpenChange] - ); + const cardName = name.trim() || getDisplayName(url); + const favicon = getFaviconUrl(url); + onCreate(url.trim(), cardName, favicon); + + // Reset form + setUrl(""); + setName(""); + onOpenChange(false); + }, [url, name, isValid, onCreate, onOpenChange]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter' && isValid) { + e.preventDefault(); + handleSubmit(); + } else if (e.key === 'Escape') { + onOpenChange(false); + } + }, [isValid, handleSubmit, onOpenChange]); // Reset form when dialog opens useEffect(() => { if (open) { - setUrlsText(""); - setIsCreating(false); - // Focus textarea after dialog opens - setTimeout(() => textareaRef.current?.focus(), 100); + setUrl(""); + setName(""); + setIsValid(false); + setIsLoadingTitle(false); + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } } }, [open]); - const handlePaste = useCallback((e: React.ClipboardEvent) => { - // Prevent default paste to handle it manually - e.preventDefault(); - - // Get pasted text - let text = e.clipboardData.getData("text"); - - // 1. Pre-process to separate mashed URLs (e.g. "http://a.comhttp://b.com") - // This adds a space before any http protocol that isn't at the very start - text = text.replace(/(? 0 && currentValue[start - 1] !== "\n") { - formattedText = "\n" + formattedText; - } - - const newValue = - currentValue.substring(0, start) + - formattedText + - currentValue.substring(end); - - setUrlsText(newValue); - - // Update cursor position (async to let render happen, though React state update might contest this) - // With standard React controlled inputs, we usually just set state. - // We can create a synthetic change event or just let the effect handle it, - // but explicit state setting is cleanest here. - - // Fix cursor position after update - setTimeout(() => { - if (textareaRef.current) { - const newCursorPos = start + formattedText.length; - textareaRef.current.selectionStart = textareaRef.current.selectionEnd = newCursorPos; - } - }, 0); - }, [urlsText]); - return ( - - + + - Create Note from Websites + Embed Website - Paste one or more website URLs (one per line). A note will be created with content synthesized from these pages. + Enter a website URL to embed it in your workspace. -
+
- -