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