'use client'
import { useState, useEffect, useMemo, useCallback } from 'react'
import { XMarkIcon, BookOpenIcon, TagIcon, PencilIcon, UserIcon, PhotoIcon, ArrowUpTrayIcon, LinkIcon, ChartBarIcon, CheckCircleIcon, ArrowTopRightOnSquareIcon, SparklesIcon, ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline'
import { useAuth } from '@/components/AuthProvider'
import Image from 'next/image'
import { supabase } from '@/lib/supabase'
import { useImageGeneration, type GeneratedImage } from '@/lib/hooks/useImageGeneration'
interface Book {
id: string
title: string
description: string | null
author?: string | null
cover_image_url?: string | null
genre?: string | null
target_word_count?: number | null
status: string
}
interface EditBookDialogProps {
isOpen: boolean
onClose: () => void
onSubmit: (bookData: { title: string; description?: string; author?: string; cover_image_url?: string; genre?: string; target_word_count?: number; status?: string }) => void
loading: boolean
book: Book | null
}
interface ImageOptionProps {
image: GeneratedImage
isSelected: boolean
onSelect: () => void
}
function ImageOption({ image, isSelected, onSelect }: ImageOptionProps) {
const [isLoading, setIsLoading] = useState(true)
return (
<button
type="button"
onClick={onSelect}
className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all duration-200 ${
isSelected
? 'border-teal-500 ring-2 ring-teal-500/50'
: 'border-slate-600 hover:border-slate-500'
}`}
>
{isLoading && (
<div className="absolute inset-0 bg-slate-700/50 animate-pulse flex items-center justify-center">
<div className="w-6 h-6 bg-white/20 rounded animate-pulse"></div>
</div>
)}
<Image
src={image.image_url}
alt={image.prompt}
fill
className="object-cover"
onLoad={() => setIsLoading(false)}
onError={() => setIsLoading(false)}
/>
{isSelected && (
<div className="absolute inset-0 bg-teal-500/20 flex items-center justify-center">
<div className="w-6 h-6 rounded-full bg-teal-500 flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
</div>
)}
</button>
)
}
export default function EditBookDialog({ isOpen, onClose, onSubmit, loading, book }: EditBookDialogProps) {
const { user } = useAuth()
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [author, setAuthor] = useState('')
const [coverImageUrl, setCoverImageUrl] = useState<string | null>(null)
const [selectedGenres, setSelectedGenres] = useState<string[]>([])
const [targetWordCount, setTargetWordCount] = useState<number | null>(null)
const [status, setStatus] = useState('')
const [showUrlInput, setShowUrlInput] = useState(false)
const [urlInput, setUrlInput] = useState('')
const [isUploading, setIsUploading] = useState(false)
const [showImageSelector, setShowImageSelector] = useState(false)
const [prompt, setPrompt] = useState('')
const [isCoverExpanded, setIsCoverExpanded] = useState(false)
const [isGenerateExpanded, setIsGenerateExpanded] = useState(false)
const { images, isGenerating, generateImage } = useImageGeneration()
const availableGenres = useMemo(() => [
'Fantasy', 'Science Fiction', 'Mystery', 'Romance', 'Thriller', 'Horror',
'Historical Fiction', 'Contemporary Fiction', 'Young Adult', "Children's",
'Biography', 'Memoir', 'Self-Help', 'Business', 'Health', 'Travel',
'Poetry', 'Drama', 'Comedy', 'Adventure', 'Crime', 'Western',
'Dystopian', 'Paranormal', 'Urban Fantasy', 'Epic Fantasy'
], [])
const sortedGenres = useMemo(() => {
return [
...selectedGenres,
...availableGenres.filter(genre => !selectedGenres.includes(genre))
]
}, [selectedGenres, availableGenres])
const wordCountOptions = [
{ value: 1000, label: '๐ Flash Fiction', description: '~1,000 words' },
{ value: 7500, label: '๐ Short Story', description: '~7,500 words' },
{ value: 17500, label: '๐ Novelette', description: '~17,500 words' },
{ value: 40000, label: '๐ Novella', description: '~40,000 words' },
{ value: 80000, label: '๐ Novel', description: '~80,000 words' },
{ value: 120000, label: '๐ Epic Novel', description: '~120,000 words' },
{ value: null, label: 'โจ Custom Target', description: 'Set your own goal' }
]
const statusOptions = [
{ value: 'Planning', label: '๐ฏ Planning', description: 'Outlining and brainstorming' },
{ value: 'draft', label: 'โ๏ธ First Draft', description: 'Getting words on paper' },
{ value: 'in_progress', label: '๐ In Progress', description: 'Actively writing' },
{ value: 'completed', label: 'โ
Completed', description: 'Ready for review' },
{ value: 'published', label: '๐ Published', description: 'Live for the world to see' }
]
// Reset form when book changes or dialog opens
useEffect(() => {
if (book && isOpen) {
setTitle(book.title || '')
setDescription(book.description || '')
setAuthor(book.author || '')
setCoverImageUrl(book.cover_image_url || null)
// Parse genres from string to array
if (book.genre) {
const genresArray = book.genre.split(',').map(g => g.trim()).filter(g => g.length > 0)
setSelectedGenres(genresArray)
} else {
setSelectedGenres([])
}
setTargetWordCount(book.target_word_count || null)
setStatus(book.status || 'draft')
}
}, [book, isOpen])
// Body scroll lock effect
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'unset'
}
return () => {
document.body.style.overflow = 'unset'
}
}, [isOpen])
const handleGenreClick = useCallback((genre: string) => {
setSelectedGenres(prev =>
prev.includes(genre)
? prev.filter(g => g !== genre)
: [...prev, genre]
)
}, [])
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!title.trim()) return
const bookData = {
title: title.trim(),
description: description.trim() || undefined,
author: author.trim() || undefined,
cover_image_url: coverImageUrl || undefined,
genre: selectedGenres.length > 0 ? selectedGenres.join(', ') : undefined,
target_word_count: targetWordCount || undefined,
status: status || undefined
}
onSubmit(bookData)
}
const resetForm = () => {
setTitle('')
setDescription('')
setAuthor('')
setCoverImageUrl(null)
setSelectedGenres([])
setTargetWordCount(null)
setStatus('')
setShowUrlInput(false)
setUrlInput('')
setShowImageSelector(false)
setPrompt('')
setIsCoverExpanded(false)
setIsGenerateExpanded(false)
}
const handleClose = () => {
if (!loading) {
resetForm()
onClose()
}
}
const handleUrlSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (urlInput.trim()) {
setCoverImageUrl(urlInput.trim())
setUrlInput('')
setShowUrlInput(false)
setShowImageSelector(false)
}
}
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
if (!file.type.startsWith('image/')) {
alert('Please select an image file')
return
}
if (file.size > 5 * 1024 * 1024) {
alert('Image size must be less than 5MB')
return
}
try {
setIsUploading(true)
const fileExt = file.name.split('.').pop()
const fileName = `${Date.now()}-${Math.random().toString(36).substring(2)}.${fileExt}`
const filePath = user?.id ? `book-covers/${user.id}/${fileName}` : `book-covers/${fileName}`
const { data, error } = await supabase.storage
.from('book-covers')
.upload(filePath, file, {
cacheControl: '3600',
upsert: false
})
if (error) {
console.error('Upload error:', error)
alert('Failed to upload image. Please try again.')
return
}
const { data: { publicUrl } } = supabase.storage
.from('book-covers')
.getPublicUrl(data.path)
setCoverImageUrl(publicUrl)
setShowImageSelector(false)
} catch (error) {
console.error('Upload error:', error)
alert('Failed to upload image. Please try again.')
} finally {
setIsUploading(false)
if (event.target) {
event.target.value = ''
}
}
}
const handleGenerateImage = async (e: React.FormEvent) => {
e.preventDefault()
if (!prompt.trim() || isGenerating) return
const generatedImage = await generateImage(prompt, book?.id)
if (generatedImage) {
setCoverImageUrl(generatedImage.image_url)
setPrompt('')
setShowImageSelector(false)
}
}
const openImagesPage = () => {
window.open('/dashboard/images', '_blank')
}
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-slate-900 rounded-2xl shadow-2xl w-full max-w-2xl border border-slate-700/50 overflow-hidden flex flex-col"
style={{
maxHeight: '85vh'
}}>
{/* Header */}
<div className="relative bg-slate-800/50 border-b border-slate-700/50 flex-shrink-0">
<div className="flex items-center justify-between p-4">
<div className="flex items-center space-x-3">
<div className="p-1.5 bg-gradient-to-r from-teal-500 to-purple-500 rounded-lg">
<BookOpenIcon className="h-5 w-5 text-white" />
</div>
<h2 className="text-xl font-semibold text-white">Edit Book</h2>
</div>
<button
onClick={handleClose}
className="p-1.5 text-slate-400 hover:text-white hover:bg-slate-700/50 rounded-lg transition-colors duration-150"
disabled={loading}
>
<XMarkIcon className="h-5 w-5" />
</button>
</div>
</div>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto">
<div className="p-6">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Cover Image Section - First */}
<div className="space-y-4">
{/* Current Image Display - Centered */}
<div className="flex justify-center">
<div className={`relative w-32 h-40 rounded-lg border-2 border-dashed overflow-hidden transition-all duration-200 ${
coverImageUrl
? 'border-slate-600 bg-slate-800/50'
: 'border-slate-600 bg-slate-800/30'
}`}>
{coverImageUrl ? (
<>
<Image
src={coverImageUrl}
alt="Book cover"
fill
className="object-cover"
/>
<button
type="button"
onClick={() => setCoverImageUrl(null)}
className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center transition-colors"
title="Remove cover image"
>
<XMarkIcon className="w-3 h-3 text-white" />
</button>
</>
) : (
<div className="w-full h-full flex items-center justify-center">
<PhotoIcon className="w-12 h-12 text-slate-500" />
</div>
)}
</div>
</div>
{/* Action Buttons - Side by Side */}
<div className="flex space-x-3">
<button
type="button"
onClick={() => setIsCoverExpanded(!isCoverExpanded)}
className="flex-1 flex items-center justify-center space-x-2 px-4 py-3 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white rounded-xl transition-all duration-200 font-medium shadow-lg hover:shadow-xl"
>
<PhotoIcon className="w-4 h-4" />
<span>Change Cover</span>
</button>
<button
type="button"
onClick={openImagesPage}
className="flex-1 flex items-center justify-center space-x-2 px-4 py-3 bg-slate-800/50 border border-slate-600 hover:bg-slate-700/50 hover:border-slate-500 text-slate-300 hover:text-white rounded-xl transition-all duration-200 font-medium"
>
<SparklesIcon className="w-4 h-4" />
<span>AI Images</span>
<ArrowTopRightOnSquareIcon className="w-3 h-3" />
</button>
</div>
{/* Collapsible Cover Options */}
{isCoverExpanded && (
<div className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-4 space-y-4">
{/* Generated Images Grid */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-sm font-semibold text-white">Your Generated Images</h4>
<button
type="button"
onClick={() => setIsGenerateExpanded(!isGenerateExpanded)}
className="text-xs text-slate-400 hover:text-white transition-colors"
>
{isGenerateExpanded ? 'Hide Generator' : 'Generate New'}
</button>
</div>
{images.length > 0 ? (
<div className="grid grid-cols-3 gap-2">
{images.slice(0, 6).map((image) => (
<ImageOption
key={image.id}
image={image}
isSelected={coverImageUrl === image.image_url}
onSelect={() => {
setCoverImageUrl(image.image_url)
setIsCoverExpanded(false)
}}
/>
))}
</div>
) : (
<div className="text-center py-4">
<SparklesIcon className="w-8 h-8 text-slate-500 mx-auto mb-2" />
<p className="text-sm text-slate-400">No generated images yet</p>
</div>
)}
</div>
{/* Generate New Image */}
{isGenerateExpanded && (
<div className="space-y-3 pt-3 border-t border-slate-700/50">
<h4 className="text-sm font-semibold text-white">Generate New Image</h4>
<form onSubmit={handleGenerateImage} className="space-y-3">
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe your book cover... (e.g., 'Fantasy novel cover with dragons and mountains')"
rows={2}
className="w-full px-3 py-2 bg-slate-800/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 resize-none focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 text-sm"
disabled={isGenerating || loading}
/>
<button
type="submit"
disabled={!prompt.trim() || isGenerating || loading}
className="w-full flex items-center justify-center space-x-2 px-4 py-2 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white rounded-lg transition-all duration-200 font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
{isGenerating ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
<span>Generating...</span>
</>
) : (
<>
<SparklesIcon className="w-4 h-4" />
<span>Generate Image</span>
</>
)}
</button>
</form>
</div>
)}
{/* Upload Options */}
<div className="space-y-3 pt-3 border-t border-slate-700/50">
<h4 className="text-sm font-semibold text-white">Upload Your Own</h4>
<div className="flex space-x-2">
<button
type="button"
onClick={() => document.getElementById('file-upload')?.click()}
disabled={isUploading || loading}
className="flex-1 flex items-center justify-center space-x-2 px-3 py-2 bg-slate-800/50 border border-slate-600 rounded-lg text-slate-300 hover:bg-slate-700/50 hover:border-slate-500 transition-colors disabled:opacity-50 text-sm"
>
<ArrowUpTrayIcon className="w-4 h-4" />
<span>{isUploading ? 'Uploading...' : 'Upload File'}</span>
</button>
<button
type="button"
onClick={() => setShowUrlInput(!showUrlInput)}
className="flex-1 flex items-center justify-center space-x-2 px-3 py-2 bg-slate-800/50 border border-slate-600 rounded-lg text-slate-300 hover:bg-slate-700/50 hover:border-slate-500 transition-colors text-sm"
>
<LinkIcon className="w-4 h-4" />
<span>Add URL</span>
</button>
</div>
{showUrlInput && (
<form onSubmit={handleUrlSubmit} className="flex space-x-2">
<input
type="url"
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
placeholder="https://example.com/image.jpg"
className="flex-1 px-3 py-2 bg-slate-800/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500"
/>
<button
type="submit"
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors text-sm"
>
Add
</button>
</form>
)}
</div>
</div>
)}
<input
id="file-upload"
type="file"
accept="image/*"
onChange={handleFileUpload}
className="hidden"
/>
</div>
{/* Title Section */}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<PencilIcon className="h-4 w-4 text-teal-400" />
<label className="text-base font-semibold text-white">Book Title</label>
</div>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-4 py-3 bg-slate-800/50 border border-slate-600 rounded-xl text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-teal-500/50 focus:border-teal-500 transition-colors duration-150"
placeholder="Enter your book title..."
required
disabled={loading}
/>
</div>
{/* Description Section */}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<BookOpenIcon className="h-4 w-4 text-purple-400" />
<label className="text-base font-semibold text-white">Description</label>
<span className="text-sm text-slate-400">(optional)</span>
</div>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
className="w-full px-4 py-3 bg-slate-800/50 border border-slate-600 rounded-xl text-white placeholder-slate-400 resize-none focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 transition-colors duration-150"
placeholder="What's your book about? A brief description helps organize your thoughts..."
disabled={loading}
/>
</div>
{/* Author Section */}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<UserIcon className="h-4 w-4 text-amber-400" />
<label className="text-base font-semibold text-white">Author</label>
<span className="text-sm text-slate-400">(optional)</span>
</div>
<input
type="text"
value={author}
onChange={(e) => setAuthor(e.target.value)}
className="w-full px-4 py-3 bg-slate-800/50 border border-slate-600 rounded-xl text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-colors duration-150"
placeholder="Pen name for this book..."
disabled={loading}
/>
</div>
{/* Genres Section */}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<TagIcon className="h-4 w-4 text-cyan-400" />
<label className="text-base font-semibold text-white">Genres</label>
<span className="text-sm text-slate-400">(optional)</span>
{selectedGenres.length > 0 && (
<span className="px-2 py-1 bg-gradient-to-r from-cyan-500/20 to-teal-500/20 text-cyan-400 text-xs rounded-full border border-cyan-500/30">
{selectedGenres.length} selected
</span>
)}
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
{sortedGenres.map((genre) => {
const isSelected = selectedGenres.includes(genre)
return (
<button
key={genre}
type="button"
onClick={() => handleGenreClick(genre)}
disabled={loading}
className={`px-3 py-2 text-sm rounded-lg border transition-colors duration-150 text-left font-medium ${
isSelected
? 'bg-gradient-to-r from-teal-600 to-cyan-600 border-teal-500 text-white'
: 'bg-slate-800/50 border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500 hover:text-white'
}`}
>
{genre}
</button>
)
})}
</div>
</div>
{/* Target Word Count Section */}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<ChartBarIcon className="h-4 w-4 text-emerald-400" />
<label className="text-base font-semibold text-white">Writing Goal</label>
<span className="text-sm text-slate-400">(optional)</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{wordCountOptions.map((option, index) => {
const isSelected = targetWordCount === option.value
const isCustom = option.value === null
return (
<button
key={index}
type="button"
onClick={() => setTargetWordCount(option.value)}
disabled={loading}
className={`p-3 rounded-lg border transition-colors duration-150 text-left ${
isSelected
? 'bg-gradient-to-r from-emerald-600 to-teal-600 border-emerald-500 text-white'
: 'bg-slate-800/50 border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500 hover:text-white'
}`}
>
<div className="font-medium text-sm">{option.label}</div>
<div className="text-xs opacity-75">{option.description}</div>
</button>
)
})}
</div>
{targetWordCount === null && (
<input
type="number"
value={targetWordCount || ''}
onChange={(e) => setTargetWordCount(e.target.value ? parseInt(e.target.value) : null)}
className="w-full px-4 py-3 bg-slate-800/50 border border-slate-600 rounded-xl text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500 transition-colors duration-150"
placeholder="Enter custom word count target..."
min="1"
disabled={loading}
/>
)}
</div>
{/* Status Section */}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<CheckCircleIcon className="h-4 w-4 text-blue-400" />
<label className="text-base font-semibold text-white">Writing Status</label>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{statusOptions.map((option) => {
const isSelected = status === option.value
return (
<button
key={option.value}
type="button"
onClick={() => setStatus(option.value)}
disabled={loading}
className={`p-3 rounded-lg border transition-colors duration-150 text-left ${
isSelected
? 'bg-gradient-to-r from-blue-600 to-purple-600 border-blue-500 text-white'
: 'bg-slate-800/50 border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500 hover:text-white'
}`}
>
<div className="font-medium text-sm">{option.label}</div>
<div className="text-xs opacity-75">{option.description}</div>
</button>
)
})}
</div>
</div>
</form>
</div>
</div>
{/* Sticky Footer */}
<div className="border-t border-slate-700/50 p-4 bg-slate-900/90 backdrop-blur-sm flex-shrink-0">
<div className="flex justify-between items-center">
<button
type="button"
onClick={handleClose}
className="px-4 py-2 text-slate-400 hover:text-white hover:bg-slate-700/50 rounded-lg transition-colors duration-150 font-medium"
disabled={loading}
>
Cancel
</button>
<button
type="submit"
onClick={handleSubmit}
className="px-6 py-2 bg-gradient-to-r from-teal-600 to-cyan-600 hover:from-teal-700 hover:to-cyan-700 text-white rounded-lg transition-colors duration-150 font-semibold shadow-lg shadow-teal-500/25 disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none flex items-center space-x-2"
disabled={!title.trim() || loading}
>
{loading && (
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
)}
<span>{loading ? 'Updating...' : 'Update Book'}</span>
</button>
</div>
</div>
</div>
</div>
)
}