bookwiz.io / components / NewBookDialog.tsx
NewBookDialog.tsx
Raw
'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&apos;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>
  )
}