captains-log/frontend/app/hooks/useApi.ts
Drew Galbraith d60d834f38 Create a frontend wireframe. (#7)
Sets up API methods and types.
Sets up a colorscheme.
Sets up a homepage.

Removes tailwind in favor of mui for now.

Reviewed-on: #7
Co-authored-by: Drew Galbraith <drew@tiramisu.one>
Co-committed-by: Drew Galbraith <drew@tiramisu.one>
2025-09-23 04:08:45 +00:00

227 lines
4.8 KiB
TypeScript

import { useState, useCallback, useRef, useEffect } from 'react'
import type { ApiError } from '~/types/task'
interface UseApiState<T> {
data: T | null
loading: boolean
error: string | null
}
interface UseApiActions<T> {
execute: (...args: unknown[]) => Promise<T | null>
reset: () => void
clearError: () => void
}
interface UseApiOptions {
immediate?: boolean
onSuccess?: (data: unknown) => void
onError?: (error: string) => void
}
export function useApi<T>(
apiFunction: (...args: unknown[]) => Promise<T>,
options: UseApiOptions = {}
): UseApiState<T> & UseApiActions<T> {
const { immediate = false, onSuccess, onError } = options
const [state, setState] = useState<UseApiState<T>>({
data: null,
loading: false,
error: null,
})
const mountedRef = useRef(true)
const abortControllerRef = useRef<AbortController | null>(null)
useEffect(() => {
return () => {
mountedRef.current = false
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
}
}, [])
const reset = useCallback(() => {
setState({
data: null,
loading: false,
error: null,
})
}, [])
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }))
}, [])
const execute = useCallback(
async (...args: unknown[]): Promise<T | null> => {
// Cancel previous request if still pending
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
abortControllerRef.current = new AbortController()
setState(prev => ({ ...prev, loading: true, error: null }))
try {
const result = await apiFunction(...args)
if (!mountedRef.current) return null
setState(prev => ({
...prev,
data: result,
loading: false,
}))
if (onSuccess) {
onSuccess(result)
}
return result
} catch (error) {
if (!mountedRef.current) return null
const apiError = error as ApiError
const errorMessage = apiError.message || 'An unknown error occurred'
setState(prev => ({
...prev,
loading: false,
error: errorMessage,
}))
if (onError) {
onError(errorMessage)
}
return null
} finally {
abortControllerRef.current = null
}
},
[apiFunction, onSuccess, onError]
)
// Execute immediately if requested
useEffect(() => {
if (immediate) {
execute()
}
}, [immediate, execute])
return {
...state,
execute,
reset,
clearError,
}
}
// Utility hook for handling form submissions
export function useApiForm<T>(
submitFunction: (data: unknown) => Promise<T>,
options: UseApiOptions & { resetOnSuccess?: boolean } = {}
) {
const { resetOnSuccess = false, ...apiOptions } = options
const api = useApi(submitFunction, apiOptions)
const handleSubmit = useCallback(
async (data: unknown) => {
const result = await api.execute(data)
if (result && resetOnSuccess) {
api.reset()
}
return result
},
[api, resetOnSuccess]
)
return {
...api,
handleSubmit,
}
}
// Utility hook for data caching and synchronization
export function useApiCache<T>(
key: string,
fetchFunction: () => Promise<T>,
options: { cacheTime?: number; staleTime?: number } = {}
) {
const { cacheTime = 5 * 60 * 1000, staleTime = 30 * 1000 } = options // 5min cache, 30s stale
const [cacheData, setCacheData] = useState<{
data: T | null
timestamp: number
} | null>(null)
const api = useApi(fetchFunction)
const getCachedData = useCallback((): T | null => {
if (!cacheData) return null
const now = Date.now()
const age = now - cacheData.timestamp
if (age > cacheTime) {
setCacheData(null)
return null
}
return cacheData.data
}, [cacheData, cacheTime])
const isStale = useCallback((): boolean => {
if (!cacheData) return true
const now = Date.now()
const age = now - cacheData.timestamp
return age > staleTime
}, [cacheData, staleTime])
const fetchData = useCallback(
async (force = false): Promise<T | null> => {
// Return cached data if fresh and not forced
if (!force && !isStale()) {
const cached = getCachedData()
if (cached) return cached
}
const result = await api.execute()
if (result) {
setCacheData({
data: result,
timestamp: Date.now(),
})
}
return result
},
[api, isStale, getCachedData]
)
const clearCache = useCallback(() => {
setCacheData(null)
api.reset()
}, [api])
return {
data: api.data || getCachedData(),
loading: api.loading,
error: api.error,
fetchData,
clearCache,
get isStale() {
return isStale()
},
}
}