bookwiz.io / app / dashboard / books / page.tsx
page.tsx
Raw
'use client'

import { useState } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { useAuth } from '@/components/AuthProvider'
import { useBooks, Book } from '@/lib/hooks/useBooks'
import DashboardLayout from '@/components/dashboard/DashboardLayout'
import BooksGridSkeleton from '@/components/dashboard/BooksGridSkeleton'
import EditBookDialog from '@/components/EditBookDialog'
import DeleteConfirmationDialog from '@/components/DeleteConfirmationDialog'
import { useNewBookDialog } from '@/lib/contexts/NewBookDialogContext'
import { 
  PlusIcon,
  CalendarDaysIcon,
  PencilSquareIcon,
  BookOpenIcon,
  TrashIcon,
  UserIcon,
} from '@heroicons/react/24/outline'

export default function DashboardBooksPage() {
  const { user } = useAuth()
  const { openDialog } = useNewBookDialog()
  const { books, loading: booksLoading, error, updateBook: updateBookInCache, deleteBook: deleteBookFromCache } = useBooks()
  const [isEditBookDialogOpen, setIsEditBookDialogOpen] = useState(false)
  const [editingBook, setEditingBook] = useState<Book | null>(null)
  const [updatingBook, setUpdatingBook] = useState(false)
  const [deletingBookId, setDeletingBookId] = useState<string | null>(null)
  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
  const [bookToDelete, setBookToDelete] = useState<Book | null>(null)

  const handleEditBook = (book: Book) => {
    setEditingBook(book)
    setIsEditBookDialogOpen(true)
  }

  const handleUpdateBook = async (bookData: { title: string; description?: string; author?: string; cover_image_url?: string; genre?: string; target_word_count?: number; status?: string }) => {
    if (!editingBook) return

    try {
      setUpdatingBook(true)
      
      const response = await fetch(`/api/books/${editingBook.id}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          ...bookData,
          userId: user?.id
        }),
      })

      if (!response.ok) {
        throw new Error('Failed to update book')
      }

      const data = await response.json()
      
      // Update the book in the cache
      updateBookInCache(editingBook.id, data.book)
      
      // Close dialog
      setIsEditBookDialogOpen(false)
      setEditingBook(null)
    } catch (error) {
      console.error('Error updating book:', error)
      // Error handling would go here - could show a toast or handle differently
    } finally {
      setUpdatingBook(false)
    }
  }

  const handleDeleteClick = (book: Book) => {
    setBookToDelete(book)
    setIsDeleteDialogOpen(true)
  }

  const handleDeleteConfirm = async () => {
    if (!bookToDelete) return

    try {
      setDeletingBookId(bookToDelete.id)
      
      const response = await fetch(`/api/books/${bookToDelete.id}?userId=${user?.id}`, {
        method: 'DELETE',
      })

      if (!response.ok) {
        throw new Error('Failed to delete book')
      }
      
      // Remove the book from the cache
      deleteBookFromCache(bookToDelete.id)
      
      // Close dialog
      setIsDeleteDialogOpen(false)
      setBookToDelete(null)
    } catch (error) {
      console.error('Error deleting book:', error)
      // Error handling would go here - could show a toast or handle differently
    } finally {
      setDeletingBookId(null)
    }
  }

  const formatDate = (dateString: string) => {
    const date = new Date(dateString)
    const now = new Date()
    const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60))
    
    if (diffInHours < 1) {
      return 'Just now'
    } else if (diffInHours < 24) {
      return `${diffInHours} hour${diffInHours > 1 ? 's' : ''} ago`
    } else if (diffInHours < 24 * 7) {
      const days = Math.floor(diffInHours / 24)
      return `${days} day${days > 1 ? 's' : ''} ago`
    } else {
      return date.toLocaleDateString('en-US', {
        month: 'short',
        day: 'numeric',
        year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
      })
    }
  }

  const getProgressPercentage = (book: Book) => {
    if (!book.target_word_count || book.target_word_count === 0) return 0
    return Math.min((book.word_count / book.target_word_count) * 100, 100)
  }

  const getStatusColor = (status: string) => {
    switch (status.toLowerCase()) {
      case 'published':
        return 'bg-emerald-500/20 text-emerald-300 border-emerald-500/30'
      case 'draft':
        return 'bg-amber-500/20 text-amber-300 border-amber-500/30'
      case 'in_progress':
        return 'bg-blue-500/20 text-blue-300 border-blue-500/30'
      case 'completed':
        return 'bg-purple-500/20 text-purple-300 border-purple-500/30'
      default:
        return 'bg-gray-500/20 text-gray-300 border-gray-500/30'
    }
  }

  const headerActions = (
    <button
      onClick={openDialog}
      className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-teal-500 to-cyan-500 hover:from-teal-600 hover:to-cyan-600 rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl"
    >
      <PlusIcon className="h-5 w-5 mr-2" />
      New Book
    </button>
  )

  return (
    <DashboardLayout
      title="My Books"
      subtitle={books.length > 0 ? `${books.length} ${books.length === 1 ? 'book' : 'books'}` : 'Start your writing journey'}
      actions={headerActions}
    >
      {error && (
        <div className="bg-red-900/20 border border-red-900/50 text-red-400 px-4 py-3 rounded-xl mb-6">
          {error}
        </div>
      )}

      {booksLoading ? (
        <BooksGridSkeleton />
      ) : books.length > 0 ? (
        <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 lg:gap-6">
          {books.map((book) => (
            <div
              key={book.id}
              className="group relative bg-gray-900/40 backdrop-blur-sm border border-gray-800/50 rounded-2xl overflow-hidden hover:bg-gray-900/60 hover:border-gray-700/50 transition-all duration-300 hover:shadow-xl hover:shadow-black/20 transform hover:-translate-y-1"
            >
              {/* Book Cover Container */}
              <Link
                href={`/books/${book.id}`}
                className="block relative w-full aspect-[2/3] overflow-hidden bg-gradient-to-br from-gray-800 to-gray-900 cursor-pointer"
              >
                <Image
                  src={book.cover_image_url || '/images/missingcover.webp'}
                  alt={`Cover for ${book.title}`}
                  fill
                  className="object-cover group-hover:scale-105 transition-transform duration-500"
                  sizes="(max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw"
                />
                
                {/* Book Spine Effect */}
                <div className="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-gray-700/50 to-gray-900/50"></div>
                
                {/* Overlay with book info */}
                <div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
                  <div className="absolute bottom-0 left-0 right-0 p-4">
                    {book.genre && (
                      <div className="flex flex-wrap gap-1 mb-1">
                        {book.genre.split(',').map((genre, index) => (
                          <span
                            key={index}
                            className="px-2 py-1 bg-cyan-500/20 text-cyan-300 text-xs rounded-full border border-cyan-500/30 font-medium"
                          >
                            {genre.trim()}
                          </span>
                        ))}
                      </div>
                    )}
                    {book.author && (
                      <p className="text-xs text-gray-300 line-clamp-1">
                        by {book.author}
                      </p>
                    )}
                  </div>
                </div>
                
                {/* Action buttons overlay */}
                <div className="absolute top-2 right-2 flex space-x-1 opacity-100 transition-all duration-200">
                  <button
                    onClick={(e) => {
                      e.preventDefault()
                      e.stopPropagation()
                      handleEditBook(book)
                    }}
                    className="p-1.5 text-white/80 hover:text-blue-400 hover:bg-blue-400/20 rounded-lg transition-all duration-200 backdrop-blur-sm"
                    title="Edit book"
                  >
                    <PencilSquareIcon className="h-3.5 w-3.5" />
                  </button>
                  <button
                    onClick={(e) => {
                      e.preventDefault()
                      e.stopPropagation()
                      handleDeleteClick(book)
                    }}
                    className="p-1.5 text-white/80 hover:text-red-400 hover:bg-red-400/20 rounded-lg transition-all duration-200 backdrop-blur-sm"
                    title="Delete book"
                    disabled={deletingBookId === book.id}
                  >
                    {deletingBookId === book.id ? (
                      <div className="animate-spin rounded-full h-3.5 w-3.5 border-2 border-red-400 border-t-transparent"></div>
                    ) : (
                      <TrashIcon className="h-3.5 w-3.5" />
                    )}
                  </button>
                </div>
              </Link>

              {/* Book Details */}
              <div className="p-4">
                <Link
                  href={`/books/${book.id}`}
                  className="block cursor-pointer"
                >
                  <h3 className="text-sm font-bold text-white mb-1 line-clamp-2 group-hover:text-teal-400 transition-colors">
                    {book.title}
                  </h3>
                  
                  {/* Author Information */}
                  {book.author && (
                    <div className="flex items-center text-amber-400 text-xs mb-2">
                      <UserIcon className="h-3 w-3 mr-1" />
                      <span className="font-medium line-clamp-1">{book.author}</span>
                    </div>
                  )}
                </Link>

                <div className="space-y-2">
                  {/* Status and date */}
                  <div className="flex items-center justify-between">
                    <span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${getStatusColor(book.status)}`}>
                      {book.status.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
                    </span>
                    <div className="flex items-center text-xs text-gray-500">
                      <CalendarDaysIcon className="h-3 w-3 mr-1" />
                      {formatDate(book.lastModified)}
                    </div>
                  </div>

                  {/* Word count and progress */}
                  <div className="flex items-center justify-between text-xs">
                    <span className="text-gray-400">
                      {book.word_count.toLocaleString()} words
                    </span>
                    {book.target_word_count && (
                      <span className="text-gray-500">
                        {getProgressPercentage(book).toFixed(0)}%
                      </span>
                    )}
                  </div>
                  
                  {/* Progress bar */}
                  {book.target_word_count && (
                    <div className="w-full bg-gray-800/50 rounded-full h-1">
                      <div
                        className="bg-gradient-to-r from-teal-500 to-cyan-500 h-1 rounded-full transition-all duration-300"
                        style={{ width: `${getProgressPercentage(book)}%` }}
                      ></div>
                    </div>
                  )}
                </div>
              </div>
            </div>
          ))}
        </div>
      ) : (
        <div className="bg-gray-900/40 backdrop-blur-sm border border-gray-800/50 rounded-2xl p-12 text-center">
          <BookOpenIcon className="h-16 w-16 text-gray-600 mx-auto mb-6" />
          <h3 className="text-2xl font-bold text-gray-300 mb-3">No books yet</h3>
          <p className="text-gray-500 mb-8 max-w-md mx-auto">
            Start your writing journey by creating your first book. Choose from our templates or start from scratch.
          </p>
          <button
            onClick={openDialog}
            className="inline-flex items-center px-6 py-3 text-sm font-medium text-white bg-gradient-to-r from-teal-500 to-cyan-500 hover:from-teal-600 hover:to-cyan-600 rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl"
          >
            <PlusIcon className="h-5 w-5 mr-2" />
            Create Your First Book
          </button>
        </div>
      )}

      {/* Edit Book Dialog */}
      <EditBookDialog
        isOpen={isEditBookDialogOpen}
        onClose={() => {
          setIsEditBookDialogOpen(false)
          setEditingBook(null)
        }}
        onSubmit={handleUpdateBook}
        loading={updatingBook}
        book={editingBook}
      />

      {/* Delete Confirmation Dialog */}
      <DeleteConfirmationDialog
        isOpen={isDeleteDialogOpen}
        onClose={() => {
          setIsDeleteDialogOpen(false)
          setBookToDelete(null)
        }}
        onConfirm={handleDeleteConfirm}
        loading={deletingBookId !== null}
        title="Delete Book"
        message={`Are you sure you want to delete "${bookToDelete?.title}"? This action cannot be undone and will permanently remove the book and all its content.`}
        confirmText="Delete Book"
      />
    </DashboardLayout>
  )
}