bookwiz.io / app / dashboard / chat / layout.tsx
layout.tsx
Raw
'use client'

import React, { useState, useEffect } from 'react'
import { useRouter, usePathname } from 'next/navigation'
import { useAuth } from '@/components/AuthProvider'
import { Chat } from '@/lib/types/database'
import { ChatContext, type ChatContextType } from '@/lib/contexts/ChatContext'
import { IoAddOutline, IoTrashOutline, IoPencilOutline, IoCheckmarkOutline, IoMenuOutline, IoCloseOutline, IoChatbubblesOutline } from 'react-icons/io5'
import DeleteConfirmationDialog from '@/components/DeleteConfirmationDialog'

interface ChatLayoutProps {
  children: React.ReactNode
}

export default function ChatLayout({ children }: ChatLayoutProps) {
  const { user } = useAuth()
  const router = useRouter()
  const pathname = usePathname()
  const [chats, setChats] = useState<Chat[]>([])
  const [currentChatId, setCurrentChatId] = useState<string | null>(null)
  const [isLoading, setIsLoading] = useState(true)
  const [editingChatId, setEditingChatId] = useState<string | null>(null)
  const [editingTitle, setEditingTitle] = useState('')
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
  const [chatToDelete, setChatToDelete] = useState<Chat | null>(null)

  // Extract chat ID from pathname
  useEffect(() => {
    const pathParts = pathname.split('/')
    const chatIdFromPath = pathParts[pathParts.length - 1]
    
    // Only set if it's a valid UUID-like string (not 'chat')
    if (chatIdFromPath && chatIdFromPath !== 'chat' && chatIdFromPath.length > 10) {
      setCurrentChatId(chatIdFromPath)
    } else {
      setCurrentChatId(null)
    }
  }, [pathname])

  // Fetch standalone chats on mount and when user changes
  useEffect(() => {
    if (user?.id) {
      fetchChats()
    }
  }, [user?.id])

  const fetchChats = async () => {
    try {
      setIsLoading(true)
      const response = await fetch(`/api/standalone-chat?userId=${user?.id}`)
      const data = await response.json()
      
      if (response.ok) {
        setChats(data.chats || [])
      } else {
        console.error('Failed to fetch chats:', data.error)
      }
    } catch (error) {
      console.error('Error fetching chats:', error)
    } finally {
      setIsLoading(false)
    }
  }

  // Optimized function to add new chat to the list without full refresh
  const addNewChatToList = (newChat: Chat) => {
    setChats(prevChats => [newChat, ...prevChats])
  }

  // Optimized function to update chat timestamp without full refresh
  const updateChatTimestamp = (chatId: string) => {
    setChats(prevChats => 
      prevChats.map(chat => 
        chat.id === chatId 
          ? { ...chat, updated_at: new Date().toISOString() }
          : chat
      ).sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
    )
  }

  const createNewChat = () => {
    router.push('/dashboard/chat')
  }

  const selectChat = (chatId: string) => {
    router.push(`/dashboard/chat/${chatId}`)
  }

  const deleteChat = async (chatId: string) => {
    try {
      const response = await fetch(`/api/standalone-chat?chatId=${chatId}&userId=${user?.id}`, {
        method: 'DELETE'
      })

      if (response.ok) {
        setChats(chats.filter(chat => chat.id !== chatId))
        if (currentChatId === chatId) {
          router.push('/dashboard/chat')
        }
      } else {
        console.error('Failed to delete chat')
      }
    } catch (error) {
      console.error('Error deleting chat:', error)
    }
  }

  const handleDeleteConfirm = async () => {
    if (chatToDelete) {
      await deleteChat(chatToDelete.id)
      setIsDeleteDialogOpen(false)
      setChatToDelete(null)
    }
  }

  const updateChatTitle = async (chatId: string, newTitle: string) => {
    try {
      // Update locally first for immediate feedback
      setChats(chats.map(chat => 
        chat.id === chatId ? { ...chat, title: newTitle } : chat
      ))

      // Then update in database
      const { supabase } = await import('@/lib/supabase')
      
      await supabase
        .from('chats')
        .update({ title: newTitle })
        .eq('id', chatId)

    } catch (error) {
      console.error('Error updating chat title:', error)
      // Revert local change on error
      fetchChats()
    }
  }

  const handleStartEdit = (chat: Chat) => {
    setEditingChatId(chat.id)
    setEditingTitle(chat.title)
  }

  const handleSaveEdit = async (chatId: string) => {
    if (editingTitle.trim()) {
      await updateChatTitle(chatId, editingTitle.trim())
    }
    setEditingChatId(null)
    setEditingTitle('')
  }

  const handleCancelEdit = () => {
    setEditingChatId(null)
    setEditingTitle('')
  }

  const formatDate = (dateString: string) => {
    const date = new Date(dateString)
    const now = new Date()
    const diffTime = Math.abs(now.getTime() - date.getTime())
    const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))

    if (diffDays === 1) {
      // Check if it's actually today (same calendar day)
      const isToday = date.toDateString() === now.toDateString()
      if (isToday) {
        return `Today at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
      }
      return 'Today'
    } else if (diffDays === 2) {
      return 'Yesterday'
    } else if (diffDays <= 7) {
      return `${diffDays - 1} days ago`
    } else {
      return date.toLocaleDateString()
    }
  }

  const contextValue: ChatContextType = {
    currentChatId,
    setCurrentChatId,
    chats,
    fetchChats: fetchChats,
    addNewChatToList,
    updateChatTimestamp
  }

  return (
    <ChatContext.Provider value={contextValue}>
      <div className="flex h-screen bg-black">
        {/* Mobile Menu Button */}
        <div className="md:hidden fixed top-4 right-20 z-50">
          <button
            onClick={() => setIsMobileMenuOpen(true)}
            className="p-3 rounded-2xl bg-black/20 backdrop-blur-xl border border-white/10 text-white hover:bg-black/40 transition-all duration-200 shadow-2xl"
          >
            <IoChatbubblesOutline className="h-5 w-5" />
          </button>
        </div>

        {/* Mobile Sidebar Overlay */}
        {isMobileMenuOpen && (
          <div 
            className="md:hidden fixed inset-0 bg-black/50 backdrop-blur-sm z-40"
            onClick={() => setIsMobileMenuOpen(false)}
          />
        )}

        {/* Chat History Sidebar */}
        <div className={`
          fixed md:relative inset-y-0 left-0 z-50
          w-64 lg:w-72 bg-black/40 backdrop-blur-2xl border-r border-white/10 flex flex-col
          transform transition-transform duration-300 ease-in-out
          ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
        `}>
          {/* Mobile Close Button */}
          <div className="md:hidden flex justify-end p-3">
            <button
              onClick={() => setIsMobileMenuOpen(false)}
              className="p-1 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
            >
              <IoCloseOutline className="w-5 h-5" />
            </button>
          </div>

          {/* Header */}
          <div className="p-3 border-b border-white/10">
            <button
              onClick={createNewChat}
              className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-white/5 text-slate-200 rounded-lg hover:bg-white/10 transition-colors text-sm border border-white/10"
            >
              <IoAddOutline className="w-4 h-4" />
              New Chat
            </button>
          </div>

          {/* Chat List */}
          <div className="flex-1 overflow-y-auto p-2 space-y-1">
            {isLoading ? (
              <div className="flex items-center justify-center py-8">
                <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
              </div>
            ) : chats.length === 0 ? (
              <div className="text-center py-8 text-slate-400 text-sm">
                <p>No chats yet</p>
                <p className="text-xs mt-1">Start a conversation to see it here</p>
              </div>
            ) : (
              chats.map((chat) => (
                <div
                  key={chat.id}
                  className={`group relative p-2 rounded-md transition-all cursor-pointer ${
                    currentChatId === chat.id
                      ? 'bg-white/10 text-white'
                      : 'text-slate-300 hover:bg-white/5'
                  }`}
                  onClick={() => !editingChatId && selectChat(chat.id)}
                >
                  {/* Chat Title */}
                  <div className="flex items-center justify-between">
                    {editingChatId === chat.id ? (
                      <div className="flex items-center gap-1 flex-1">
                        <input
                          type="text"
                          value={editingTitle}
                          onChange={(e) => setEditingTitle(e.target.value)}
                          className="flex-1 bg-slate-700 text-slate-200 text-xs px-2 py-1 rounded border border-slate-600 focus:outline-none focus:border-slate-400"
                          autoFocus
                          onKeyDown={(e) => {
                            if (e.key === 'Enter') {
                              handleSaveEdit(chat.id)
                            } else if (e.key === 'Escape') {
                              handleCancelEdit()
                            }
                          }}
                        />
                        <button
                          onClick={(e) => {
                            e.stopPropagation()
                            handleSaveEdit(chat.id)
                          }}
                          className="p-1 text-slate-400 hover:text-slate-200"
                        >
                          <IoCheckmarkOutline className="w-3 h-3" />
                        </button>
                      </div>
                    ) : (
                      <>
                        <h3 className="text-sm font-medium line-clamp-1 flex-1 pr-2">
                          {chat.title}
                        </h3>
                        
                        {/* Action Buttons */}
                        <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
                          <button
                            onClick={(e) => {
                              e.stopPropagation()
                              handleStartEdit(chat)
                            }}
                            className="p-1 text-slate-400 hover:text-slate-200 hover:bg-white/10 rounded"
                            title="Rename chat"
                          >
                            <IoPencilOutline className="w-3 h-3" />
                          </button>
                          <button
                            onClick={(e) => {
                              e.stopPropagation()
                              setChatToDelete(chat)
                              setIsDeleteDialogOpen(true)
                            }}
                            className="p-1 text-slate-400 hover:text-red-400 hover:bg-white/10 rounded"
                            title="Delete chat"
                          >
                            <IoTrashOutline className="w-3 h-3" />
                          </button>
                        </div>
                      </>
                    )}
                  </div>

                  {/* Chat metadata */}
                  <div className="text-xs text-slate-400 mt-1">
                    {formatDate(chat.updated_at)}
                  </div>
                </div>
              ))
            )}
          </div>
        </div>

        {/* Chat Content */}
        <div className="flex-1 flex flex-col md:ml-0">
          {children}
        </div>
      </div>

      {/* Delete Confirmation Dialog */}
      <DeleteConfirmationDialog
        isOpen={isDeleteDialogOpen}
        onClose={() => {
          setIsDeleteDialogOpen(false)
          setChatToDelete(null)
        }}
        onConfirm={handleDeleteConfirm}
        loading={false}
        title="Delete Chat"
        message={`Delete "${chatToDelete?.title}"? This action cannot be undone.`}
        confirmText="Delete Chat"
      />
    </ChatContext.Provider>
  )
}