'use client'; import { useState, useEffect } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useAuth } from '../../contexts/AuthContext'; import { Input, Textarea } from '../../components/ui/Input'; import Button from '../../components/ui/Button'; import TagInput from '../../components/stories/TagInput'; import PortableTextEditor from '../../components/stories/PortableTextEditorNew'; import ImageUpload from '../../components/ui/ImageUpload'; import AuthorSelector from '../../components/stories/AuthorSelector'; import SeriesSelector from '../../components/stories/SeriesSelector'; import { storyApi, authorApi } from '../../lib/api'; export default function AddStoryContent() { const [formData, setFormData] = useState({ title: '', summary: '', authorName: '', authorId: undefined as string | undefined, contentHtml: '', sourceUrl: '', tags: [] as string[], seriesName: '', seriesId: undefined as string | undefined, volume: '', }); const [coverImage, setCoverImage] = useState(null); const [loading, setLoading] = useState(false); const [processingImages, setProcessingImages] = useState(false); const [errors, setErrors] = useState>({}); const [duplicateWarning, setDuplicateWarning] = useState<{ show: boolean; count: number; duplicates: Array<{ id: string; title: string; authorName: string; createdAt: string; }>; }>({ show: false, count: 0, duplicates: [] }); const [checkingDuplicates, setCheckingDuplicates] = useState(false); const router = useRouter(); const searchParams = useSearchParams(); const { isAuthenticated } = useAuth(); // Handle URL parameters useEffect(() => { const authorId = searchParams.get('authorId'); const from = searchParams.get('from'); // Pre-fill author if authorId is provided in URL if (authorId) { const loadAuthor = async () => { try { const author = await authorApi.getAuthor(authorId); setFormData(prev => ({ ...prev, authorName: author.name, authorId: author.id })); } catch (error) { console.error('Failed to load author:', error); } }; loadAuthor(); } // Handle URL import data if (from === 'url-import') { const title = searchParams.get('title') || ''; const summary = searchParams.get('summary') || ''; const author = searchParams.get('author') || ''; const sourceUrl = searchParams.get('sourceUrl') || ''; const tagsParam = searchParams.get('tags'); const content = searchParams.get('content') || ''; let tags: string[] = []; try { tags = tagsParam ? JSON.parse(tagsParam) : []; } catch (error) { console.error('Failed to parse tags:', error); tags = []; } setFormData(prev => ({ ...prev, title, summary, authorName: author, authorId: undefined, // Reset author ID when importing from URL contentHtml: content, sourceUrl, tags })); // Show success message setErrors({ success: 'Story data imported successfully! Review and edit as needed before saving.' }); } }, [searchParams]); // Load pending story data from bulk combine operation useEffect(() => { const fromBulkCombine = searchParams.get('from') === 'bulk-combine'; if (fromBulkCombine) { const pendingStoryData = localStorage.getItem('pendingStory'); if (pendingStoryData) { try { const storyData = JSON.parse(pendingStoryData); setFormData(prev => ({ ...prev, title: storyData.title || '', authorName: storyData.author || '', authorId: undefined, // Reset author ID for bulk combined stories contentHtml: storyData.content || '', sourceUrl: storyData.sourceUrl || '', summary: storyData.summary || '', tags: storyData.tags || [] })); // Clear the pending data localStorage.removeItem('pendingStory'); } catch (error) { console.error('Failed to load pending story data:', error); } } } }, [searchParams]); // Check for duplicates when title and author are both present useEffect(() => { const checkDuplicates = async () => { const title = formData.title.trim(); const authorName = formData.authorName.trim(); // Don't check if user isn't authenticated or if title/author are empty if (!isAuthenticated || !title || !authorName) { setDuplicateWarning({ show: false, count: 0, duplicates: [] }); return; } // Debounce the check to avoid too many API calls const timeoutId = setTimeout(async () => { try { setCheckingDuplicates(true); const result = await storyApi.checkDuplicate(title, authorName); if (result.hasDuplicates) { setDuplicateWarning({ show: true, count: result.count, duplicates: result.duplicates }); } else { setDuplicateWarning({ show: false, count: 0, duplicates: [] }); } } catch (error) { console.error('Failed to check for duplicates:', error); // Clear any existing duplicate warnings on error setDuplicateWarning({ show: false, count: 0, duplicates: [] }); // Don't show error to user as this is just a helpful warning // Authentication errors will be handled by the API interceptor } finally { setCheckingDuplicates(false); } }, 500); // 500ms debounce return () => clearTimeout(timeoutId); }; checkDuplicates(); }, [formData.title, formData.authorName, isAuthenticated]); const handleInputChange = (field: string) => ( e: React.ChangeEvent ) => { setFormData(prev => ({ ...prev, [field]: e.target.value })); // Clear error when user starts typing if (errors[field]) { setErrors(prev => ({ ...prev, [field]: '' })); } }; const handleContentChange = (html: string) => { setFormData(prev => ({ ...prev, contentHtml: html })); if (errors.contentHtml) { setErrors(prev => ({ ...prev, contentHtml: '' })); } }; const handleTagsChange = (tags: string[]) => { setFormData(prev => ({ ...prev, tags })); }; const handleAuthorChange = (authorName: string, authorId?: string) => { setFormData(prev => ({ ...prev, authorName, authorId: authorId // This will be undefined if creating new author, which clears the existing ID })); // Clear error when user changes author if (errors.authorName) { setErrors(prev => ({ ...prev, authorName: '' })); } }; const handleSeriesChange = (seriesName: string, seriesId?: string) => { setFormData(prev => ({ ...prev, seriesName, seriesId: seriesId // This will be undefined if creating new series, which clears the existing ID })); // Clear error when user changes series if (errors.seriesName) { setErrors(prev => ({ ...prev, seriesName: '' })); } }; const validateForm = () => { const newErrors: Record = {}; if (!formData.title.trim()) { newErrors.title = 'Title is required'; } if (!formData.authorName.trim()) { newErrors.authorName = 'Author name is required'; } if (!formData.contentHtml.trim()) { newErrors.contentHtml = 'Story content is required'; } if (formData.seriesName && !formData.volume) { newErrors.volume = 'Volume number is required when series is specified'; } if (formData.volume && !formData.seriesName.trim()) { newErrors.seriesName = 'Series name is required when volume is specified'; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; // Helper function to detect external images in HTML content const hasExternalImages = (htmlContent: string): boolean => { if (!htmlContent) return false; // Create a temporary DOM element to parse HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; const images = tempDiv.querySelectorAll('img'); for (let i = 0; i < images.length; i++) { const img = images[i]; const src = img.getAttribute('src'); if (src && (src.startsWith('http://') || src.startsWith('https://'))) { return true; } } return false; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validateForm()) { return; } setLoading(true); try { // First, create the story with JSON data const storyData = { title: formData.title, summary: formData.summary || undefined, contentHtml: formData.contentHtml, sourceUrl: formData.sourceUrl || undefined, volume: formData.seriesName ? parseInt(formData.volume) : undefined, // Send seriesId if we have it (existing series), otherwise send seriesName (new series) ...(formData.seriesId ? { seriesId: formData.seriesId } : { seriesName: formData.seriesName || undefined }), // Send authorId if we have it (existing author), otherwise send authorName (new author) ...(formData.authorId ? { authorId: formData.authorId } : { authorName: formData.authorName }), tagNames: formData.tags.length > 0 ? formData.tags : undefined, }; const story = await storyApi.createStory(storyData); // Process images if there are external images in the content if (hasExternalImages(formData.contentHtml)) { try { setProcessingImages(true); const imageResult = await storyApi.processContentImages(story.id, formData.contentHtml); // If images were processed and content was updated, save the updated content if (imageResult.processedContent !== formData.contentHtml) { await storyApi.updateStory(story.id, { title: formData.title, summary: formData.summary || undefined, contentHtml: imageResult.processedContent, sourceUrl: formData.sourceUrl || undefined, volume: formData.seriesName ? parseInt(formData.volume) : undefined, ...(formData.seriesId ? { seriesId: formData.seriesId } : { seriesName: formData.seriesName || undefined }), ...(formData.authorId ? { authorId: formData.authorId } : { authorName: formData.authorName }), tagNames: formData.tags.length > 0 ? formData.tags : undefined, }); // Show success message with image processing info if (imageResult.downloadedImages.length > 0) { console.log(`Successfully processed ${imageResult.downloadedImages.length} images`); } if (imageResult.warnings && imageResult.warnings.length > 0) { console.warn('Image processing warnings:', imageResult.warnings); } } } catch (imageError) { console.error('Failed to process images:', imageError); // Don't fail the entire operation if image processing fails // The story was created successfully, just without processed images } finally { setProcessingImages(false); } } // If there's a cover image, upload it separately if (coverImage) { await storyApi.uploadCover(story.id, coverImage); } router.push(`/stories/${story.id}/detail`); } catch (error: any) { console.error('Failed to create story:', error); const errorMessage = error.response?.data?.message || 'Failed to create story'; setErrors({ submit: errorMessage }); } finally { setLoading(false); } }; return ( <> {/* Success Message */} {errors.success && (

{errors.success}

)}
{/* Title */} {/* Author Selector */} {/* Duplicate Warning */} {duplicateWarning.show && (
⚠️

Potential Duplicate Detected

Found {duplicateWarning.count} existing {duplicateWarning.count === 1 ? 'story' : 'stories'} with the same title and author:

    {duplicateWarning.duplicates.map((duplicate, index) => (
  • {duplicate.title} by {duplicate.authorName} (added {new Date(duplicate.createdAt).toLocaleDateString()})
  • ))}

You can still create this story if it's different from the existing ones.

)} {/* Checking indicator */} {checkingDuplicates && (
Checking for duplicates...
)} {/* Summary */}