bookwiz.io / lib / hooks / useFileSystem.ts
useFileSystem.ts
Raw
import { useState, useCallback, useEffect, useRef } from 'react'
import { supabase } from '@/lib/supabase'
import type { FileSystemItem, CreateFileSystemItemRequest, UpdateFileSystemItemRequest } from '@/lib/types/database'

export interface FileSystemState {
  files: (FileSystemItem & { children?: FileSystemItem[] })[]
  loading: boolean
  error: string | null
}

export interface UseFileSystemReturn extends FileSystemState {
  fetchFiles: (bookId: string) => Promise<void>
  createItem: (bookId: string, data: CreateFileSystemItemRequest) => Promise<FileSystemItem | null>
  updateItem: (bookId: string, fileId: string, data: UpdateFileSystemItemRequest) => Promise<FileSystemItem | null>
  deleteItem: (bookId: string, fileId: string) => Promise<boolean>
  refreshFiles: () => Promise<void>
  forceRefreshFiles: () => Promise<void>
}

// Helper function to get auth headers for API requests
const getAuthHeaders = async () => {
  const { data: { session } } = await supabase.auth.getSession()
  const headers: Record<string, string> = { 'Content-Type': 'application/json' }
  
  if (session?.access_token) {
    headers['Authorization'] = `Bearer ${session.access_token}`
  }
  
  return headers
}

// Helper function to transform flat array into hierarchical tree
function buildFileTree(items: FileSystemItem[]): (FileSystemItem & { children?: FileSystemItem[] })[] {
  const itemMap = new Map<string, FileSystemItem & { children: FileSystemItem[] }>()
  const roots: (FileSystemItem & { children: FileSystemItem[] })[] = []

  // Create map of all items with children arrays
  items.forEach(item => {
    itemMap.set(item.id, { ...item, children: [] })
  })

  // Build tree structure
  items.forEach(item => {
    const itemWithChildren = itemMap.get(item.id)
    if (!itemWithChildren) return

    if (item.parent_id) {
      const parent = itemMap.get(item.parent_id)
      if (parent) {
        parent.children.push(itemWithChildren)
      }
    } else {
      roots.push(itemWithChildren)
    }
  })

  // Sort children recursively
  const sortItems = (items: (FileSystemItem & { children: FileSystemItem[] })[]) => {
    items.sort((a, b) => {
      // Folders first, then files
      if (a.type !== b.type) {
        return a.type === 'folder' ? -1 : 1
      }
      // Then by sort_order, then by name
      if (a.sort_order !== b.sort_order) {
        return (a.sort_order || 0) - (b.sort_order || 0)
      }
      return a.name.localeCompare(b.name)
    })
    
    // Recursively sort children
    items.forEach(item => {
      if (item.children.length > 0) {
        sortItems(item.children as (FileSystemItem & { children: FileSystemItem[] })[])
      }
    })
  }

  sortItems(roots)
  return roots
}

export function useFileSystem(): UseFileSystemReturn {
  const [state, setState] = useState<FileSystemState>({
    files: [],
    loading: false,
    error: null
  })
  const [currentBookId, setCurrentBookId] = useState<string | null>(null)
  const lastFetchTimeRef = useRef<number>(0)
  const isPageVisibleRef = useRef<boolean>(true)

  // Handle page visibility to prevent unnecessary fetches during tab switches
  useEffect(() => {
    const handleVisibilityChange = () => {
      isPageVisibleRef.current = !document.hidden
      
      if (!document.hidden) {
        const timeSinceLastFetch = Date.now() - lastFetchTimeRef.current
        
        // Only refresh if it's been more than 30 seconds since last fetch
        if (currentBookId && timeSinceLastFetch > 30000) {
          fetchFiles(currentBookId)
        }
      }
    }

    document.addEventListener('visibilitychange', handleVisibilityChange)
    return () => document.removeEventListener('visibilitychange', handleVisibilityChange)
  }, [currentBookId])

  const fetchFiles = useCallback(async (bookId: string) => {
    // Skip fetch if page is hidden unless it's a forced refresh
    if (!isPageVisibleRef.current && lastFetchTimeRef.current > 0) {
      return
    }

    try {
      lastFetchTimeRef.current = Date.now()
      
      const { data: items, error } = await supabase
        .from('file_system_items')
        .select('*')
        .eq('book_id', bookId)
        .order('sort_order', { ascending: true })

      if (error) {
        throw new Error(error.message || 'Failed to fetch files')
      }

      const fileTree = buildFileTree(items || [])
      
      setState(prev => ({
        ...prev,
        files: fileTree,
        loading: false,
        error: null
      }))
    } catch (error) {
      console.error('Error fetching files:', error)
      setState(prev => ({
        ...prev,
        error: error instanceof Error ? error.message : 'Failed to fetch files',
        loading: false
      }))
    }
  }, [])

  // Initial fetch when bookId changes
  useEffect(() => {
    if (currentBookId) {
      setState(prev => ({ ...prev, loading: true }))
      fetchFiles(currentBookId)
    }
  }, [currentBookId, fetchFiles])

  // Enhanced realtime listener with visibility-aware updates
  useEffect(() => {
    if (!currentBookId) return
    
    const channel = supabase
      .channel(`files_${currentBookId}`)
      .on(
        'postgres_changes',
        {
          event: '*',
          schema: 'public',
          table: 'file_system_items',
          filter: `book_id=eq.${currentBookId}`
        },
        (payload) => {          
          // Always process realtime updates, but prioritize when page is visible
          if (isPageVisibleRef.current) {
            setTimeout(() => fetchFiles(currentBookId), 100)
          } else {
            // Still update but with a longer delay for background updates
            setTimeout(() => {
              if (isPageVisibleRef.current) {
                fetchFiles(currentBookId)
              }
            }, 1000)
          }
        }
      )
      .subscribe((status) => {
        if (status === 'CLOSED') {
          console.error('๐Ÿ“ก Realtime subscription closed for files')
        }
      })

    return () => {
      supabase.removeChannel(channel)
    }
  }, [currentBookId, fetchFiles])

  const createItem = useCallback(async (
    bookId: string, 
    data: CreateFileSystemItemRequest
  ): Promise<FileSystemItem | null> => {
    try {
      const headers = await getAuthHeaders()
      const response = await fetch(`/api/books/${bookId}/files`, {
        method: 'POST',
        headers,
        body: JSON.stringify(data)
      })
      
      if (!response.ok) {
        const errorData = await response.json()
        throw new Error(errorData.error || 'Failed to create item')
      }
      
      const result = await response.json()
      
      // Reduced fallback delay since realtime is now faster
      setTimeout(() => fetchFiles(bookId), 100)
      
      return result.item
    } catch (error) {
      console.error('Error creating item:', error)
      setState(prev => ({
        ...prev,
        error: error instanceof Error ? error.message : 'Failed to create item'
      }))
      return null
    }
  }, [fetchFiles])

  const updateItem = useCallback(async (
    bookId: string,
    fileId: string,
    data: UpdateFileSystemItemRequest
  ): Promise<FileSystemItem | null> => {
    try {
      const headers = await getAuthHeaders()
      const response = await fetch(`/api/books/${bookId}/files/${fileId}`, {
        method: 'PUT',
        headers,
        body: JSON.stringify(data)
      })
      
      if (!response.ok) {
        const errorData = await response.json()
        throw new Error(errorData.error || 'Failed to update item')
      }
      
      const result = await response.json()
      
      // Reduced fallback delay since realtime is now faster
      setTimeout(() => fetchFiles(bookId), 100)
      
      return result.item
    } catch (error) {
      console.error('Error updating item:', error)
      setState(prev => ({
        ...prev,
        error: error instanceof Error ? error.message : 'Failed to update item'
      }))
      return null
    }
  }, [fetchFiles])

  const deleteItem = useCallback(async (
    bookId: string,
    fileId: string
  ): Promise<boolean> => {
    try {
      const headers = await getAuthHeaders()
      const response = await fetch(`/api/books/${bookId}/files/${fileId}`, {
        method: 'DELETE',
        headers
      })
      
      if (!response.ok) {
        const errorData = await response.json()
        throw new Error(errorData.error || 'Failed to delete item')
      }
      
      // Reduced fallback delay
      setTimeout(() => fetchFiles(bookId), 100)
      
      return true
    } catch (error) {
      console.error('Error deleting item:', error)
      setState(prev => ({
        ...prev,
        error: error instanceof Error ? error.message : 'Failed to delete item'
      }))
      return false
    }
  }, [fetchFiles])

  const refreshFiles = useCallback(async () => {
    if (currentBookId) {
      setState(prev => ({ ...prev, loading: true }))
      await fetchFiles(currentBookId)
    }
  }, [currentBookId, fetchFiles])
  
  const forceRefreshFiles = useCallback(async () => {
    if (currentBookId) {
      console.log('๐Ÿ”„ Force refreshing files for book:', currentBookId)
      setState(prev => ({ ...prev, loading: true }))
      
      try {
        lastFetchTimeRef.current = Date.now()
        
        const { data: items, error } = await supabase
          .from('file_system_items')
          .select('*')
          .eq('book_id', currentBookId)
          .order('sort_order', { ascending: true })

        if (error) {
          throw new Error(error.message || 'Failed to fetch files')
        }

        const fileTree = buildFileTree(items || [])
        
        setState(prev => ({
          ...prev,
          files: fileTree,
          loading: false,
          error: null
        }))
        
        console.log('โœ… Force refresh completed, found', items?.length || 0, 'items')
      } catch (error) {
        console.error('โŒ Error in force refresh:', error)
        setState(prev => ({
          ...prev,
          error: error instanceof Error ? error.message : 'Failed to fetch files',
          loading: false
        }))
      }
    }
  }, [currentBookId])

  return {
    ...state,
    fetchFiles: useCallback(async (bookId: string) => {
      setCurrentBookId(bookId)
      setState(prev => ({ ...prev, loading: true }))
      await fetchFiles(bookId)
    }, [fetchFiles]),
    createItem,
    updateItem,
    deleteItem,
    refreshFiles,
    forceRefreshFiles
  }
}