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>
This commit is contained in:
parent
7d2b7fc90c
commit
d60d834f38
27 changed files with 3114 additions and 977 deletions
227
frontend/app/hooks/useApi.ts
Normal file
227
frontend/app/hooks/useApi.ts
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
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()
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue