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>
227 lines
4.8 KiB
TypeScript
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()
|
|
},
|
|
}
|
|
}
|