'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>
)
}