'use client'
import { useState, useEffect, useMemo, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { XMarkIcon, SparklesIcon, BookOpenIcon, TagIcon, PencilIcon, ArrowLeftIcon, ChevronDownIcon, ChevronRightIcon, UserIcon } from '@heroicons/react/24/outline'
import { BookTemplate } from '@/lib/types/database'
import TemplateSelector from './TemplateSelector'
import TemplatePreview from './TemplatePreview'
interface NewBookDialogProps {
isOpen: boolean
onClose: () => void
onSubmit: (bookData: { title: string; description?: string; author?: string; genres?: string[]; template_id?: string }) => Promise<any>
loading?: boolean
}
export default function NewBookDialog({ isOpen, onClose, onSubmit, loading = false }: NewBookDialogProps) {
const router = useRouter()
const [step, setStep] = useState<'basic' | 'template'>('basic')
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [author, setAuthor] = useState('')
const [selectedGenres, setSelectedGenres] = useState<string[]>([])
const [templates, setTemplates] = useState<BookTemplate[]>([])
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null)
const [templatesLoading, setTemplatesLoading] = useState(false)
const [isCreating, setIsCreating] = useState(false)
const [isNavigating, setIsNavigating] = useState(false)
const [error, setError] = useState<string | null>(null)
const [limitError, setLimitError] = useState<{
message: string
currentUsage: number
limit: number
feature: string
} | null>(null)
// New state for collapsible sections and surprise me feature
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const [isAuthorExpanded, setIsAuthorExpanded] = useState(false)
const [isGenresExpanded, setIsGenresExpanded] = useState(false)
const [isSurpriseMeLoading, setIsSurpriseMeLoading] = useState(false)
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'
], [])
// Memoize sorted genres for better performance - only recompute when selectedGenres changes
const sortedGenres = useMemo(() => {
return [
...selectedGenres,
...availableGenres.filter(genre => !selectedGenres.includes(genre))
]
}, [selectedGenres, availableGenres])
// Body scroll lock effect
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'unset'
}
// Cleanup on unmount
return () => {
document.body.style.overflow = 'unset'
}
}, [isOpen])
// Surprise me functionality
const handleSurpriseMe = useCallback(async () => {
setIsSurpriseMeLoading(true)
let retryCount = 0
const maxRetries = 3
while (retryCount < maxRetries) {
try {
const response = await fetch('/api/books/surprise-me', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error('Failed to generate book idea')
}
const bookIdea = await response.json()
if (bookIdea.error) {
throw new Error(bookIdea.error)
}
setTitle(bookIdea.title)
setDescription(bookIdea.description)
setSelectedGenres(bookIdea.genres || [])
// Expand sections if they have content
if (bookIdea.description) setIsDescriptionExpanded(true)
if (bookIdea.genres && bookIdea.genres.length > 0) setIsGenresExpanded(true)
// Success - break out of retry loop
break
} catch (error) {
console.error(`Error generating book idea (attempt ${retryCount + 1}):`, error)
retryCount++
if (retryCount >= maxRetries) {
// All retries failed - show error to user
setError('Failed to generate book idea. Please try again.')
// Set a simple fallback
setTitle('My New Book')
setDescription('')
setSelectedGenres([])
} else {
// Wait a bit before retrying
await new Promise(resolve => setTimeout(resolve, 500))
}
}
}
setIsSurpriseMeLoading(false)
}, [])
// Memoize fetch templates function to prevent unnecessary re-renders
const fetchTemplates = useCallback(async () => {
if (templates.length > 0) return // Don't fetch if already loaded
try {
setTemplatesLoading(true)
const response = await fetch('/api/templates')
if (!response.ok) throw new Error('Failed to fetch templates')
const data = await response.json()
setTemplates(data.templates || [])
// Auto-select default template
const defaultTemplate = data.templates?.find((t: BookTemplate) => t.is_default)
if (defaultTemplate) {
setSelectedTemplateId(defaultTemplate.id)
}
} catch (error) {
console.error('Error fetching templates:', error)
} finally {
setTemplatesLoading(false)
}
}, [templates.length])
// Optimize genre selection
const handleGenreClick = useCallback((genre: string) => {
setSelectedGenres(prev =>
prev.includes(genre)
? prev.filter(g => g !== genre)
: [...prev, genre]
)
}, [])
const handleBasicSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault()
if (!title.trim()) return
setStep('template')
// Only fetch templates when needed
fetchTemplates()
}, [title, fetchTemplates])
const handleBack = useCallback(() => {
if (step === 'template') {
setStep('basic')
}
}, [step])
const handleClose = useCallback(() => {
if (isCreating || isNavigating) return // Don't allow closing during creation or navigation
setTitle('')
setDescription('')
setAuthor('')
setSelectedGenres([])
setSelectedTemplateId(null)
setStep('basic')
setIsCreating(false)
setIsNavigating(false)
setError(null)
setLimitError(null)
setIsDescriptionExpanded(false)
setIsAuthorExpanded(false)
setIsGenresExpanded(false)
onClose()
}, [isCreating, isNavigating, onClose])
const handleFinalSubmit = useCallback(async () => {
if (isCreating || isNavigating) return // Prevent double submission
setIsCreating(true)
setError(null)
setLimitError(null)
try {
const bookData = {
title: title.trim(),
description: description.trim() || undefined,
author: author.trim() || undefined,
genres: selectedGenres.length > 0 ? selectedGenres : undefined,
template_id: selectedTemplateId || undefined
}
// Create the book and get the result
const result = await onSubmit(bookData)
// Set navigating state to keep dialog open during navigation
setIsNavigating(true)
setIsCreating(false) // Book creation is done
// Navigate to the new book
router.push(`/books/${result.book.id}`)
// Don't close dialog here - it will close when the new page loads
// The component will unmount when we navigate away
} catch (error: any) {
setIsCreating(false)
setIsNavigating(false)
// Handle specific limit exceeded errors
if (error?.limitExceeded && error?.usageInfo) {
setLimitError({
message: error.error || 'You have reached your book creation limit.',
currentUsage: error.usageInfo.currentUsage,
limit: error.usageInfo.limit,
feature: error.usageInfo.feature
})
} else {
// Handle general errors
setError(error?.message || error?.error || 'Failed to create book. Please try again.')
}
console.error('Error creating book:', error)
}
}, [isCreating, isNavigating, title, description, selectedGenres, selectedTemplateId, onSubmit, router])
const handleStepClick = useCallback((targetStep: 'basic' | 'template') => {
// Only allow going to template step if basic info is filled
if (targetStep === 'template' && !title.trim()) return
setStep(targetStep)
if (targetStep === 'template') {
fetchTemplates()
}
}, [title, fetchTemplates])
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-4xl border border-slate-700/50 overflow-hidden flex flex-col"
style={{
height: '85vh',
maxHeight: '800px'
}}>
{/* Header with Step Navigation */}
<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-6">
<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>
</div>
{/* Centered Step Navigation */}
<div className="flex items-center space-x-1">
<button
onClick={() => handleStepClick('basic')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors duration-150 ${
step === 'basic'
? 'bg-teal-500/20 text-teal-300 border border-teal-500/30'
: 'text-slate-400 hover:text-white hover:bg-slate-700/50'
}`}
disabled={isCreating || isNavigating}
>
1. Details
</button>
<div className="w-8 h-px bg-slate-600"></div>
<button
onClick={() => handleStepClick('template')}
disabled={!title.trim() || isCreating || isNavigating}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors duration-150 ${
step === 'template'
? 'bg-purple-500/20 text-purple-300 border border-purple-500/30'
: title.trim()
? 'text-slate-400 hover:text-white hover:bg-slate-700/50'
: 'text-slate-600 cursor-not-allowed'
}`}
>
2. Template
</button>
</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={isCreating || isNavigating}
>
<XMarkIcon className="h-5 w-5" />
</button>
</div>
{/* Progress indicator */}
<div className="absolute bottom-0 left-0 w-full h-0.5 bg-slate-700/50">
<div
className="h-full bg-gradient-to-r from-teal-500 to-purple-500 transition-all duration-300 ease-out"
style={{ width: step === 'basic' ? '50%' : '100%' }}
/>
</div>
</div>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto">
{/* Step 1: Basic Information */}
{step === 'basic' && (
<div className="p-6">
<form onSubmit={handleBasicSubmit} className="space-y-6">
{/* Title Section with Surprise Me Button */}
<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">What's your book called?</label>
</div>
<div className="relative">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-4 py-3 pr-32 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 || isCreating || isNavigating || isSurpriseMeLoading}
autoFocus
/>
<button
type="button"
onClick={handleSurpriseMe}
disabled={loading || isCreating || isNavigating || isSurpriseMeLoading}
className="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1.5 text-purple-400 hover:text-purple-300 hover:bg-slate-700/50 rounded-lg transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1.5 text-sm font-medium"
title="Generate a random book idea"
>
{isSurpriseMeLoading ? (
<>
<div className="animate-spin rounded-full h-3.5 w-3.5 border-2 border-purple-400 border-t-transparent"></div>
<span>Generating...</span>
</>
) : (
<>
<SparklesIcon className="h-3.5 w-3.5" />
<span>Surprise me</span>
</>
)}
</button>
</div>
<p className="text-xs text-slate-400 flex items-center space-x-1">
<SparklesIcon className="h-3 w-3" />
<span>Click the magic button for instant book ideas!</span>
</p>
</div>
{/* Collapsible Description Section */}
<div className="space-y-3">
<button
type="button"
onClick={() => setIsDescriptionExpanded(!isDescriptionExpanded)}
className="flex items-center justify-between w-full text-left group"
>
<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">Tell us more about it</label>
<span className="text-sm text-slate-400">(optional)</span>
</div>
<div className="flex items-center space-x-2">
{description && (
<span className="px-2 py-1 bg-purple-500/20 text-purple-400 text-xs rounded-full border border-purple-500/30">
Added
</span>
)}
{isDescriptionExpanded ? (
<ChevronDownIcon className="h-4 w-4 text-slate-400 group-hover:text-white transition-colors" />
) : (
<ChevronRightIcon className="h-4 w-4 text-slate-400 group-hover:text-white transition-colors" />
)}
</div>
</button>
{isDescriptionExpanded && (
<div className="space-y-2">
<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 || isCreating || isNavigating}
/>
</div>
)}
</div>
{/* Collapsible Author Section */}
<div className="space-y-3">
<button
type="button"
onClick={() => setIsAuthorExpanded(!isAuthorExpanded)}
className="flex items-center justify-between w-full text-left group"
>
<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">Who's the author?</label>
<span className="text-sm text-slate-400">(optional)</span>
</div>
<div className="flex items-center space-x-2">
{author && (
<span className="px-2 py-1 bg-amber-500/20 text-amber-400 text-xs rounded-full border border-amber-500/30">
Added
</span>
)}
{isAuthorExpanded ? (
<ChevronDownIcon className="h-4 w-4 text-slate-400 group-hover:text-white transition-colors" />
) : (
<ChevronRightIcon className="h-4 w-4 text-slate-400 group-hover:text-white transition-colors" />
)}
</div>
</button>
{isAuthorExpanded && (
<div className="space-y-2">
<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 (e.g., your author name or pseudonym)..."
disabled={loading || isCreating || isNavigating}
/>
</div>
)}
</div>
{/* Collapsible Genres Section */}
<div className="space-y-3">
<button
type="button"
onClick={() => setIsGenresExpanded(!isGenresExpanded)}
className="flex items-center justify-between w-full text-left group"
>
<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">Pick your genres</label>
<span className="text-sm text-slate-400">(optional)</span>
</div>
<div className="flex items-center space-x-2">
{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>
)}
{isGenresExpanded ? (
<ChevronDownIcon className="h-4 w-4 text-slate-400 group-hover:text-white transition-colors" />
) : (
<ChevronRightIcon className="h-4 w-4 text-slate-400 group-hover:text-white transition-colors" />
)}
</div>
</button>
{isGenresExpanded && (
<div className="space-y-2">
<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 || isCreating || isNavigating}
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>
)}
</div>
</form>
</div>
)}
{/* Step 2: Template Selection */}
{step === 'template' && (
<div className="p-6">
{/* Template Selection Content */}
<div className="space-y-6">
{/* Template Grid */}
<div className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<TemplateSelector
templates={templates}
selectedTemplateId={selectedTemplateId}
onTemplateSelect={setSelectedTemplateId}
loading={templatesLoading}
/>
</div>
<div className="lg:sticky lg:top-0">
<TemplatePreview
template={templates.find(t => t.id === selectedTemplateId) || null}
/>
</div>
</div>
</div>
</div>
</div>
)}
</div>
{/* Error Display */}
{(error || limitError) && (
<div className="border-t border-slate-700/50 px-4 py-3 bg-red-900/20 border-b border-red-700/50 flex-shrink-0">
{limitError ? (
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-5 h-5 bg-red-600 rounded-full flex items-center justify-center mt-0.5">
<span className="text-white text-xs font-bold">!</span>
</div>
<div className="flex-1">
<p className="text-red-300 text-sm font-medium">{limitError.message}</p>
<div className="mt-1 text-xs text-red-400">
Current usage: {limitError.currentUsage}/{limitError.limit} books
</div>
<div className="mt-2">
<a
href="/pricing"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-3 py-1.5 bg-gradient-to-r from-teal-600 to-cyan-600 hover:from-teal-700 hover:to-cyan-700 text-white text-xs font-medium rounded-lg transition-colors duration-150"
>
Upgrade Plan
<SparklesIcon className="w-3 h-3 ml-1" />
</a>
</div>
</div>
<button
type="button"
onClick={() => setLimitError(null)}
className="flex-shrink-0 text-red-400 hover:text-red-300 transition-colors"
>
<XMarkIcon className="w-4 h-4" />
</button>
</div>
) : (
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-5 h-5 bg-red-600 rounded-full flex items-center justify-center mt-0.5">
<span className="text-white text-xs font-bold">!</span>
</div>
<div className="flex-1">
<p className="text-red-300 text-sm">{error}</p>
</div>
<button
type="button"
onClick={() => setError(null)}
className="flex-shrink-0 text-red-400 hover:text-red-300 transition-colors"
>
<XMarkIcon className="w-4 h-4" />
</button>
</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={handleBack}
disabled={step === 'basic' || isCreating || isNavigating}
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:opacity-50 disabled:cursor-not-allowed disabled:hover:text-slate-400 disabled:hover:bg-transparent flex items-center space-x-2"
>
<ArrowLeftIcon className="h-4 w-4" />
<span>Back</span>
</button>
{step === 'basic' ? (
<button
type="submit"
onClick={handleBasicSubmit}
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"
disabled={!title.trim() || loading || isCreating || isNavigating}
>
Continue to Templates
</button>
) : (
<button
type="button"
onClick={handleFinalSubmit}
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={!selectedTemplateId || isCreating || isNavigating}
>
{(isCreating || isNavigating) && (
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
)}
<span>
{isCreating ? 'Creating Your Book...' : isNavigating ? 'Redirecting...' : 'Create Book'}
</span>
</button>
)}
</div>
</div>
</div>
</div>
)
}