feat: auto-fetch title and description when URL is pasted

- Automatically fetch page metadata using r.jina.ai proxy
- Debounced (800ms) to avoid API spam while typing
- Only auto-fills if fields are empty (won't overwrite user input)
- Extracts title from page
- Extracts description from meta tag or first paragraph
- Shows spinner indicator while fetching
- Gracefully handles fetch errors (just skips auto-fill)
- Uses existing fetchReadableContent service
This commit is contained in:
Gigi
2025-10-08 11:00:51 +01:00
parent 1a84817453
commit 82977fa5d4
2 changed files with 93 additions and 4 deletions

View File

@@ -1,7 +1,8 @@
import React, { useState } from 'react'
import React, { useState, useEffect, useRef } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTimes } from '@fortawesome/free-solid-svg-icons'
import { faTimes, faSpinner } from '@fortawesome/free-solid-svg-icons'
import IconButton from './IconButton'
import { fetchReadableContent } from '../services/readerService'
interface AddBookmarkModalProps {
onClose: () => void
@@ -14,7 +15,77 @@ const AddBookmarkModal: React.FC<AddBookmarkModalProps> = ({ onClose, onSave })
const [description, setDescription] = useState('')
const [tagsInput, setTagsInput] = useState('')
const [isSaving, setIsSaving] = useState(false)
const [isFetchingMetadata, setIsFetchingMetadata] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchTimeoutRef = useRef<number | null>(null)
// Fetch metadata when URL changes
useEffect(() => {
// Clear any pending fetch
if (fetchTimeoutRef.current) {
clearTimeout(fetchTimeoutRef.current)
}
// Don't fetch if URL is empty or invalid
if (!url.trim()) return
// Validate URL format first
let parsedUrl: URL
try {
parsedUrl = new URL(url.trim())
} catch {
return // Invalid URL, don't fetch
}
// Debounce the fetch to avoid spamming the API
fetchTimeoutRef.current = window.setTimeout(async () => {
setIsFetchingMetadata(true)
try {
const metadata = await fetchReadableContent(parsedUrl.toString())
// Only auto-fill if fields are empty
if (metadata.title && !title) {
setTitle(metadata.title)
}
// Try to extract description from markdown or HTML
if (!description) {
let extractedDesc = ''
if (metadata.markdown) {
// Take first paragraph from markdown
const firstPara = metadata.markdown.split('\n\n')[0]
extractedDesc = firstPara.replace(/^#+\s*/g, '').trim().slice(0, 200)
} else if (metadata.html) {
// Try to extract meta description or first paragraph
const metaMatch = metadata.html.match(/<meta\s+name=["']description["']\s+content=["']([^"']+)["']/i)
if (metaMatch) {
extractedDesc = metaMatch[1]
} else {
// Fallback to first <p> tag
const pMatch = metadata.html.match(/<p[^>]*>(.*?)<\/p>/is)
if (pMatch) {
extractedDesc = pMatch[1].replace(/<[^>]+>/g, '').trim().slice(0, 200)
}
}
}
if (extractedDesc) {
setDescription(extractedDesc)
}
}
} catch (err) {
console.warn('Failed to fetch metadata:', err)
// Don't show error to user, just skip auto-fill
} finally {
setIsFetchingMetadata(false)
}
}, 800) // Wait 800ms after user stops typing
return () => {
if (fetchTimeoutRef.current) {
clearTimeout(fetchTimeoutRef.current)
}
}
}, [url]) // Only depend on url, not title/description to avoid loops
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
@@ -73,7 +144,14 @@ const AddBookmarkModal: React.FC<AddBookmarkModalProps> = ({ onClose, onSave })
<form onSubmit={handleSubmit} className="modal-form">
<div className="form-group">
<label htmlFor="bookmark-url">URL *</label>
<label htmlFor="bookmark-url">
URL *
{isFetchingMetadata && (
<span className="fetching-indicator">
<FontAwesomeIcon icon={faSpinner} spin /> Fetching details...
</span>
)}
</label>
<input
id="bookmark-url"
type="text"

View File

@@ -2243,13 +2243,24 @@ body {
}
.form-group label {
display: block;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
color: #ccc;
font-size: 0.9rem;
font-weight: 500;
}
.fetching-indicator {
font-size: 0.8rem;
color: #999;
font-weight: normal;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.form-group input,
.form-group textarea {
width: 100%;