bookwiz.io / components / CoverImageSelector.tsx
CoverImageSelector.tsx
Raw
'use client'

import { useState, useRef } from 'react'
import Image from 'next/image'
import { 
  PhotoIcon, 
  PlusIcon, 
  XMarkIcon,
  LinkIcon,
  SparklesIcon,
  ArrowUpTrayIcon
} from '@heroicons/react/24/outline'
import { useImageGeneration, type GeneratedImage } from '@/lib/hooks/useImageGeneration'
import { supabase } from '@/lib/supabase'

interface CoverImageSelectorProps {
  currentImageUrl?: string | null
  onImageSelect: (imageUrl: string | null) => void
  bookId?: string
  userId?: string
}

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 CoverImageSelector({ currentImageUrl, onImageSelect, bookId, userId }: CoverImageSelectorProps) {
  const [isExpanded, setIsExpanded] = useState(false)
  const [urlInput, setUrlInput] = useState('')
  const [showUrlInput, setShowUrlInput] = useState(false)
  const [prompt, setPrompt] = useState('')
  const [isUploading, setIsUploading] = useState(false)
  const fileInputRef = useRef<HTMLInputElement>(null)
  
  const { images, isGenerating, generateImage } = useImageGeneration()

  const handleUrlSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (urlInput.trim()) {
      onImageSelect(urlInput.trim())
      setUrlInput('')
      setShowUrlInput(false)
      setIsExpanded(false)
    }
  }

  const handleGenerateImage = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!prompt.trim() || isGenerating) return
    
    const generatedImage = await generateImage(prompt, bookId)
    if (generatedImage) {
      onImageSelect(generatedImage.image_url)
      setPrompt('')
      setIsExpanded(false)
    }
  }

  const handleRemoveImage = () => {
    onImageSelect(null)
    setIsExpanded(false)
  }

  const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0]
    if (!file) return

    // Validate file type
    if (!file.type.startsWith('image/')) {
      alert('Please select an image file')
      return
    }

    // Validate file size (max 5MB)
    if (file.size > 5 * 1024 * 1024) {
      alert('Image size must be less than 5MB')
      return
    }

    try {
      setIsUploading(true)

      // Generate unique filename
      const fileExt = file.name.split('.').pop()
      const fileName = `${Date.now()}-${Math.random().toString(36).substring(2)}.${fileExt}`
      const filePath = userId ? `book-covers/${userId}/${fileName}` : `book-covers/${fileName}`

      // Upload to Supabase Storage
      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
      }

      // Get public URL
      const { data: { publicUrl } } = supabase.storage
        .from('book-covers')
        .getPublicUrl(data.path)

      // Set the uploaded image URL
      onImageSelect(publicUrl)
      setIsExpanded(false)

    } catch (error) {
      console.error('Upload error:', error)
      alert('Failed to upload image. Please try again.')
    } finally {
      setIsUploading(false)
      // Reset file input
      if (fileInputRef.current) {
        fileInputRef.current.value = ''
      }
    }
  }

  const handleUploadClick = () => {
    fileInputRef.current?.click()
  }

  if (!isExpanded) {
    return (
      <div className="space-y-3">
        <div className="flex items-center space-x-2">
          <PhotoIcon className="h-4 w-4 text-purple-400" />
          <label className="text-base font-semibold text-white">Cover Image</label>
          <span className="text-sm text-slate-400">(optional)</span>
        </div>
        
        <div className="flex items-center space-x-3">
          {/* Current Image Display */}
          <div 
            className={`relative w-20 h-20 rounded-lg border-2 border-dashed overflow-hidden transition-all duration-200 ${
              currentImageUrl 
                ? 'border-slate-600 bg-slate-800/50' 
                : 'border-slate-600 bg-slate-800/30 hover:border-slate-500 hover:bg-slate-800/50'
            }`}
          >
            {currentImageUrl ? (
              <>
                <Image
                  src={currentImageUrl}
                  alt="Book cover"
                  fill
                  className="object-cover"
                />
                <button
                  type="button"
                  onClick={handleRemoveImage}
                  className="absolute -top-1 -right-1 w-6 h-6 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-8 h-8 text-slate-500" />
              </div>
            )}
          </div>
          
          {/* Expand Button */}
          <button
            type="button"
            onClick={() => setIsExpanded(true)}
            className="flex items-center space-x-2 px-4 py-2 bg-slate-800/50 hover:bg-slate-700/50 text-slate-300 hover:text-white rounded-lg border border-slate-600 hover:border-slate-500 transition-all duration-200"
          >
            <PlusIcon className="w-4 h-4" />
            <span className="text-sm font-medium">
              {currentImageUrl ? 'Change Cover' : 'Add Cover'}
            </span>
          </button>
        </div>
      </div>
    )
  }

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between">
        <div className="flex items-center space-x-2">
          <PhotoIcon className="h-4 w-4 text-purple-400" />
          <label className="text-base font-semibold text-white">Choose Cover Image</label>
        </div>
        <button
          type="button"
          onClick={() => setIsExpanded(false)}
          className="p-1 text-slate-400 hover:text-white rounded"
        >
          <XMarkIcon className="w-4 h-4" />
        </button>
      </div>

      <div className="space-y-6">
        {/* Generated Images Grid */}
        {images.length > 0 && (
          <div className="space-y-3">
            <h4 className="text-sm font-medium text-slate-300">Your Generated Images</h4>
            <div className="grid grid-cols-4 gap-3">
              {images.slice(0, 8).map((image) => (
                <ImageOption
                  key={image.id}
                  image={image}
                  isSelected={currentImageUrl === image.image_url}
                  onSelect={() => {
                    onImageSelect(image.image_url)
                    setIsExpanded(false)
                  }}
                />
              ))}
            </div>
          </div>
        )}

        {/* Upload Image */}
        <div className="space-y-3">
          <h4 className="text-sm font-medium text-slate-300">Upload Your Own Image</h4>
          <input
            ref={fileInputRef}
            type="file"
            accept="image/*"
            onChange={handleFileUpload}
            className="hidden"
          />
          <button
            type="button"
            onClick={handleUploadClick}
            disabled={isUploading}
            className="w-full px-4 py-3 bg-slate-800/50 hover:bg-slate-700/50 border border-slate-600 hover:border-slate-500 text-slate-300 hover:text-white rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2 text-sm border-dashed"
          >
            {isUploading ? (
              <>
                <ArrowUpTrayIcon className="w-4 h-4 animate-pulse" />
                <span>Uploading...</span>
              </>
            ) : (
              <>
                <ArrowUpTrayIcon className="w-4 h-4" />
                <span>Choose Image File</span>
              </>
            )}
          </button>
          <p className="text-xs text-slate-500 text-center">
            Supports JPG, PNG, WebP up to 5MB
          </p>
        </div>

        {/* Generate New Image */}
        <div className="space-y-3">
                                  <h4 className="text-sm font-medium text-slate-300">Generate New Cover</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')"
              className="w-full px-3 py-2 bg-slate-800/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 resize-none text-sm"
              rows={2}
              disabled={isGenerating}
            />
            <button
              type="submit"
              disabled={!prompt.trim() || isGenerating}
              className="w-full px-4 py-2 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-medium rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2 text-sm"
            >
              {isGenerating ? (
                <>
                  <SparklesIcon className="w-4 h-4 animate-spin" />
                  <span>Generating...</span>
                </>
              ) : (
                <>
                  <SparklesIcon className="w-4 h-4" />
                  <span>Generate Image</span>
                </>
              )}
            </button>
          </form>
        </div>

        {/* URL Input */}
        <div className="space-y-3">
          <div className="flex items-center justify-between">
            <h4 className="text-sm font-medium text-slate-300">Or Use Image URL</h4>
            <button
              type="button"
              onClick={() => setShowUrlInput(!showUrlInput)}
              className="text-xs text-slate-400 hover:text-slate-300"
            >
              {showUrlInput ? 'Hide' : 'Show'}
            </button>
          </div>
          
          {showUrlInput && (
            <form onSubmit={handleUrlSubmit} className="space-y-3">
              <input
                type="url"
                value={urlInput}
                onChange={(e) => setUrlInput(e.target.value)}
                placeholder="https://example.com/image.jpg"
                className="w-full px-3 py-2 bg-slate-800/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 text-sm"
              />
              <button
                type="submit"
                disabled={!urlInput.trim()}
                className="w-full px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white font-medium rounded-lg transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2 text-sm"
              >
                <LinkIcon className="w-4 h-4" />
                <span>Use This URL</span>
              </button>
            </form>
          )}
        </div>

        {/* Actions */}
        <div className="flex space-x-3 pt-2">
          <button
            type="button"
            onClick={handleRemoveImage}
            className="flex-1 px-4 py-2 bg-red-600/20 hover:bg-red-600/30 text-red-400 font-medium rounded-lg transition-colors duration-200 text-sm"
            disabled={!currentImageUrl}
          >
            Remove Cover
          </button>
          <button
            type="button"
            onClick={() => setIsExpanded(false)}
            className="flex-1 px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white font-medium rounded-lg transition-colors duration-200 text-sm"
          >
            Done
          </button>
        </div>
      </div>
    </div>
  )
}