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 createItem: (bookId: string, data: CreateFileSystemItemRequest) => Promise updateItem: (bookId: string, fileId: string, data: UpdateFileSystemItemRequest) => Promise deleteItem: (bookId: string, fileId: string) => Promise refreshFiles: () => Promise forceRefreshFiles: () => Promise } // Helper function to get auth headers for API requests const getAuthHeaders = async () => { const { data: { session } } = await supabase.auth.getSession() const headers: Record = { '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() 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({ files: [], loading: false, error: null }) const [currentBookId, setCurrentBookId] = useState(null) const lastFetchTimeRef = useRef(0) const isPageVisibleRef = useRef(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 => { 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 => { 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 => { 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 } }