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

import { useState } from 'react'
import Image from 'next/image'
import { 
  IoImageOutline, 
  IoDownloadOutline, 
  IoTrashOutline, 
  IoSettingsOutline,
  IoCloseOutline,
  IoSparklesOutline,
  IoTimeOutline,
  IoSendOutline
} from 'react-icons/io5'
import { useImageGeneration, type GeneratedImage, type ImageGenerationSettings } from '@/lib/hooks/useImageGeneration'

interface ImageGalleryProps {
  isOpen: boolean
  onClose: () => void
  bookId?: string
}

interface AdvancedSettingsProps {
  settings: ImageGenerationSettings
  onSettingsChange: (settings: Partial<ImageGenerationSettings>) => void
  isOpen: boolean
  onToggle: () => void
}

function AdvancedSettings({ settings, onSettingsChange, isOpen, onToggle }: AdvancedSettingsProps) {
  if (!isOpen) {
    return (
      <button
        onClick={onToggle}
        className="flex items-center gap-2 px-3 py-2 text-sm text-slate-400 hover:text-slate-200 hover:bg-slate-700/50 rounded-lg transition-all duration-200"
      >
        <IoSettingsOutline className="w-4 h-4" />
        Advanced Settings
      </button>
    )
  }

  return (
    <div className="space-y-4 p-4 bg-slate-800/50 rounded-xl border border-slate-700/50">
      <div className="flex items-center justify-between">
        <h3 className="text-sm font-semibold text-slate-200">Advanced Settings</h3>
        <button
          onClick={onToggle}
          className="p-1 text-slate-400 hover:text-slate-200 hover:bg-slate-700/50 rounded transition-colors"
        >
          <IoCloseOutline className="w-4 h-4" />
        </button>
      </div>

      {/* Size Settings */}
      <div>
        <label className="block text-sm font-medium text-slate-300 mb-2">Image Size</label>
        <div className="grid grid-cols-3 gap-2">
          {[
            { value: '1024x1024', label: 'Square', desc: '1:1' },
            { value: '1536x1024', label: 'Landscape', desc: '3:2' },
            { value: '1024x1536', label: 'Portrait', desc: '2:3' }
          ].map((size) => (
            <button
              key={size.value}
              onClick={() => onSettingsChange({ size: size.value as any })}
              className={`p-3 text-center rounded-lg border transition-all duration-200 ${
                settings.size === size.value
                  ? 'border-blue-500 bg-blue-500/10 text-blue-400'
                  : 'border-slate-600 hover:border-slate-500 text-slate-300'
              }`}
            >
              <div className="font-medium text-sm">{size.label}</div>
              <div className="text-xs text-slate-500">{size.desc}</div>
            </button>
          ))}
        </div>
      </div>

      {/* Quality Settings */}
      <div>
        <label className="block text-sm font-medium text-slate-300 mb-2">Quality</label>
        <div className="grid grid-cols-2 gap-2">
          {[
            { value: 'auto', label: 'Auto', desc: 'Optimal quality' },
            { value: 'high', label: 'High', desc: 'Best quality' }
          ].map((quality) => (
            <button
              key={quality.value}
              onClick={() => onSettingsChange({ quality: quality.value as any })}
              className={`p-3 text-center rounded-lg border transition-all duration-200 ${
                settings.quality === quality.value
                  ? 'border-blue-500 bg-blue-500/10 text-blue-400'
                  : 'border-slate-600 hover:border-slate-500 text-slate-300'
              }`}
            >
              <div className="font-medium text-sm">{quality.label}</div>
              <div className="text-xs text-slate-500">{quality.desc}</div>
            </button>
          ))}
        </div>
      </div>
    </div>
  )
}

function ImageCard({ image, onDelete }: { image: GeneratedImage; onDelete: (id: string) => void }) {
  const [isImageLoading, setIsImageLoading] = useState(true)
  
  const handleDownload = async () => {
    try {
      const response = await fetch(image.image_url)
      const blob = await response.blob()
      const url = URL.createObjectURL(blob)
      
      const link = document.createElement('a')
      link.href = url
      link.download = `bookwiz-generated-${image.id}.png`
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)
      URL.revokeObjectURL(url)
    } catch (error) {
      // Silently fail for download errors
    }
  }

  return (
    <div className="group relative bg-slate-800/50 rounded-xl border border-slate-700/50 overflow-hidden hover:border-slate-600/50 transition-all duration-200">
      <div className="aspect-square relative">
        {isImageLoading && (
          <div className="absolute inset-0 bg-slate-700/50 animate-pulse flex items-center justify-center">
            <div className="w-8 h-8 bg-white/20 rounded animate-pulse"></div>
          </div>
        )}
        <Image
          src={image.image_url}
          alt={image.prompt}
          fill
          className="object-cover"
          onLoad={() => setIsImageLoading(false)}
          onError={() => setIsImageLoading(false)}
        />
        
        {/* Overlay with actions */}
        <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center gap-2">
          <button
            onClick={handleDownload}
            className="p-2 bg-white/20 backdrop-blur-sm rounded-lg hover:bg-white/30 transition-colors"
            title="Download image"
          >
            <IoDownloadOutline className="w-5 h-5 text-white" />
          </button>
          <button
            onClick={() => onDelete(image.id)}
            className="p-2 bg-red-500/20 backdrop-blur-sm rounded-lg hover:bg-red-500/30 transition-colors"
            title="Delete image"
          >
            <IoTrashOutline className="w-5 h-5 text-red-400" />
          </button>
        </div>
      </div>
      
      {/* Image info */}
      <div className="p-3">
        <p className="text-sm text-slate-300 line-clamp-2 mb-2">{image.prompt}</p>
        <div className="flex items-center justify-between text-xs text-slate-500">
          <span className="flex items-center gap-1">
            <IoTimeOutline className="w-3 h-3" />
            {new Date(image.created_at).toLocaleDateString()}
          </span>
          <span className="flex items-center gap-1">
            {image.size}  {image.quality}
          </span>
        </div>
      </div>
    </div>
  )
}

export default function ImageGallery({ isOpen, onClose, bookId }: ImageGalleryProps) {
  const [prompt, setPrompt] = useState('')
  const [showAdvancedSettings, setShowAdvancedSettings] = useState(false)
  
  const {
    images,
    isGenerating,
    isLoading,
    error,
    settings,
    generateImage,
    deleteImage,
    updateSettings,
    clearError
  } = useImageGeneration()

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!prompt.trim() || isGenerating) return

    const generatedImage = await generateImage(prompt, bookId)
    if (generatedImage) {
      setPrompt('') // Clear the prompt on successful generation
    }
  }

  if (!isOpen) return null

  return (
    <div className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm">
      <div className="flex">
        {/* Click outside to close */}
        <div className="flex-1" onClick={onClose} />
        
        {/* Sidebar */}
        <div className="w-96 h-screen bg-slate-900/95 backdrop-blur-xl border-l border-slate-700/50 flex flex-col">
          {/* Header */}
          <div className="p-6 border-b border-slate-700/50">
            <div className="flex items-center justify-between mb-4">
              <div className="flex items-center gap-3">
                <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center">
                  <IoImageOutline className="w-5 h-5 text-white" />
                </div>
                <div>
                  <h2 className="text-lg font-bold text-white">Image Gallery</h2>
                  <p className="text-sm text-slate-400">AI-generated images</p>
                </div>
              </div>
              <button 
                onClick={onClose}
                className="p-2 text-slate-400 hover:text-slate-200 hover:bg-slate-700/50 rounded-lg transition-colors"
              >
                <IoCloseOutline className="w-5 h-5" />
              </button>
            </div>

            {/* Prompt Input */}
            <form onSubmit={handleSubmit} className="space-y-3">
              <div>
                <textarea
                  value={prompt}
                  onChange={(e) => setPrompt(e.target.value)}
                  placeholder="Describe the image you want to generate..."
                  className="w-full p-3 bg-slate-800/50 border border-slate-700/50 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-blue-500/50 focus:ring-1 focus:ring-blue-500/50 resize-none"
                  rows={3}
                  disabled={isGenerating}
                />
              </div>
              
              <div className="flex items-center justify-between">
                <AdvancedSettings
                  settings={settings}
                  onSettingsChange={updateSettings}
                  isOpen={showAdvancedSettings}
                  onToggle={() => setShowAdvancedSettings(!showAdvancedSettings)}
                />
                
                <button
                  type="submit"
                  disabled={!prompt.trim() || isGenerating}
                  className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-purple-500 to-pink-500 text-white font-semibold rounded-lg hover:from-purple-600 hover:to-pink-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
                >
                  {isGenerating ? (
                    <>
                      <IoSparklesOutline className="w-4 h-4 animate-spin" />
                      Generating...
                    </>
                  ) : (
                    <>
                      <IoSendOutline className="w-4 h-4" />
                      Generate
                    </>
                  )}
                </button>
              </div>
            </form>

            {/* Error Display */}
            {error && (
              <div className="mt-3 p-3 bg-red-500/10 border border-red-500/20 rounded-lg">
                <div className="flex items-center justify-between">
                  <p className="text-sm text-red-400">{error}</p>
                  <button
                    onClick={clearError}
                    className="text-red-400 hover:text-red-300"
                  >
                    <IoCloseOutline className="w-4 h-4" />
                  </button>
                </div>
              </div>
            )}
          </div>

          {/* Images Grid */}
          <div className="flex-1 overflow-y-auto p-4">
            {isLoading ? (
              <div className="flex items-center justify-center h-32">
                <div className="flex items-center gap-2 text-slate-400">
                  <IoSparklesOutline className="w-5 h-5 animate-spin" />
                  Loading images...
                </div>
              </div>
            ) : images.length === 0 ? (
              <div className="flex flex-col items-center justify-center h-32 text-center">
                <IoImageOutline className="w-12 h-12 text-slate-600 mb-3" />
                <p className="text-slate-400 mb-1">No images yet</p>
                <p className="text-sm text-slate-500">Generate your first AI image above</p>
              </div>
            ) : (
              <div className="grid grid-cols-1 gap-4">
                {images.map((image) => (
                  <ImageCard
                    key={image.id}
                    image={image}
                    onDelete={deleteImage}
                  />
                ))}
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  )
}