scraping and improvements

This commit is contained in:
Stefan Hardegger
2025-07-28 13:52:09 +02:00
parent f95d7aa8bb
commit fcad028959
31 changed files with 3788 additions and 118 deletions

View File

@@ -12,6 +12,9 @@ import ImageUpload from '../../components/ui/ImageUpload';
import { storyApi, authorApi } from '../../lib/api';
export default function AddStoryPage() {
const [importMode, setImportMode] = useState<'manual' | 'url'>('manual');
const [importUrl, setImportUrl] = useState('');
const [scraping, setScraping] = useState(false);
const [formData, setFormData] = useState({
title: '',
summary: '',
@@ -130,6 +133,57 @@ export default function AddStoryPage() {
setFormData(prev => ({ ...prev, tags }));
};
const handleImportFromUrl = async () => {
if (!importUrl.trim()) {
setErrors({ importUrl: 'URL is required' });
return;
}
setScraping(true);
setErrors({});
try {
const response = await fetch('/scrape/story', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: importUrl }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to scrape story');
}
const scrapedStory = await response.json();
// Pre-fill the form with scraped data
setFormData({
title: scrapedStory.title || '',
summary: scrapedStory.summary || '',
authorName: scrapedStory.author || '',
contentHtml: scrapedStory.content || '',
sourceUrl: scrapedStory.sourceUrl || importUrl,
tags: scrapedStory.tags || [],
seriesName: '',
volume: '',
});
// Switch to manual mode so user can edit the pre-filled data
setImportMode('manual');
setImportUrl('');
// Show success message
setErrors({ success: 'Story data imported successfully! Review and edit as needed before saving.' });
} catch (error: any) {
console.error('Failed to import story:', error);
setErrors({ importUrl: error.message });
} finally {
setScraping(false);
}
};
const validateForm = () => {
const newErrors: Record<string, string> = {};
@@ -206,7 +260,105 @@ export default function AddStoryPage() {
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Import Mode Toggle */}
<div className="mb-8">
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button
type="button"
onClick={() => setImportMode('manual')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
importMode === 'manual'
? 'border-theme-accent text-theme-accent'
: 'border-transparent theme-text hover:text-theme-accent'
}`}
>
Manual Entry
</button>
<button
type="button"
onClick={() => setImportMode('url')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
importMode === 'url'
? 'border-theme-accent text-theme-accent'
: 'border-transparent theme-text hover:text-theme-accent'
}`}
>
Import from URL
</button>
</div>
</div>
{/* URL Import Section */}
{importMode === 'url' && (
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-6 mb-8">
<h3 className="text-lg font-medium theme-header mb-4">Import Story from URL</h3>
<p className="theme-text text-sm mb-4">
Enter a URL from a supported story site to automatically extract the story content, title, author, and other metadata.
</p>
<div className="space-y-4">
<Input
label="Story URL"
type="url"
value={importUrl}
onChange={(e) => setImportUrl(e.target.value)}
placeholder="https://example.com/story-url"
error={errors.importUrl}
disabled={scraping}
/>
<div className="flex gap-3">
<Button
type="button"
onClick={handleImportFromUrl}
loading={scraping}
disabled={!importUrl.trim() || scraping}
>
{scraping ? 'Importing...' : 'Import Story'}
</Button>
<Button
type="button"
variant="ghost"
onClick={() => setImportMode('manual')}
disabled={scraping}
>
Enter Manually Instead
</Button>
</div>
<div className="border-t pt-4 mt-4">
<p className="text-sm theme-text mb-2">
Need to import multiple stories at once?
</p>
<Button
type="button"
variant="secondary"
onClick={() => router.push('/stories/import/bulk')}
disabled={scraping}
size="sm"
>
Bulk Import Multiple URLs
</Button>
</div>
<div className="text-xs theme-text">
<p className="font-medium mb-1">Supported Sites:</p>
<p>Archive of Our Own, DeviantArt, FanFiction.Net, Literotica, Royal Road, Wattpad, and more</p>
</div>
</div>
</div>
)}
{/* Success Message */}
{errors.success && (
<div className="p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg mb-6">
<p className="text-green-800 dark:text-green-200">{errors.success}</p>
</div>
)}
{importMode === 'manual' && (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Title */}
<Input
label="Title *"
@@ -379,6 +531,7 @@ export default function AddStoryPage() {
</Button>
</div>
</form>
)}
</div>
</AppLayout>
);