Files
skybridge/web-components/src/hooks/useApiService.ts
2025-08-31 23:27:52 -04:00

223 lines
6.6 KiB
TypeScript

import { useState, useCallback } from 'react';
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { ApiResponse, PaginatedResponse, FilterOptions } from '../types';
export interface ApiServiceConfig {
baseURL: string;
defaultHeaders?: Record<string, string>;
timeout?: number;
}
export interface UseApiServiceReturn<T> {
data: T[];
loading: boolean;
error: string | null;
total: number;
hasMore: boolean;
client: AxiosInstance;
// CRUD operations
getAll: (filters?: FilterOptions) => Promise<T[]>;
getById: (id: string) => Promise<T>;
create: (data: Partial<T>) => Promise<T>;
update: (id: string, data: Partial<T>) => Promise<T>;
delete: (id: string) => Promise<void>;
// Utility methods
clearError: () => void;
refresh: () => Promise<void>;
}
export const useApiService = <T extends { id: string }>(
config: ApiServiceConfig,
endpoint: string
): UseApiServiceReturn<T> => {
const [data, setData] = useState<T[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [total, setTotal] = useState(0);
const [hasMore, setHasMore] = useState(false);
// Create axios instance
const client = axios.create({
baseURL: config.baseURL,
timeout: config.timeout || 10000,
headers: {
'Content-Type': 'application/json',
...config.defaultHeaders,
},
});
// Add request interceptor for common headers
client.interceptors.request.use(
(config) => {
// Add user email header if available (common pattern in the codebase)
const userEmail = 'admin@example.com'; // This could come from a context or config
if (userEmail) {
config.headers['X-User-Email'] = userEmail;
}
return config;
},
(error) => Promise.reject(error)
);
// Add response interceptor for error handling
client.interceptors.response.use(
(response) => response,
(error) => {
const errorMessage = error.response?.data?.message || error.message || 'An error occurred';
setError(errorMessage);
return Promise.reject(error);
}
);
const clearError = useCallback(() => {
setError(null);
}, []);
const getAll = useCallback(async (filters: FilterOptions = {}): Promise<T[]> => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams();
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
params.append(key, value.toString());
}
});
const response = await client.get<PaginatedResponse<T> | ApiResponse<T[]>>(`${endpoint}?${params.toString()}`);
// Handle both paginated and simple array responses
if ('data' in response.data && Array.isArray(response.data.data)) {
// Paginated response
const paginatedData = response.data as PaginatedResponse<T>;
setData(paginatedData.data);
setTotal(paginatedData.total);
setHasMore(paginatedData.has_more || false);
return paginatedData.data;
} else if ('data' in response.data && Array.isArray(response.data.data)) {
// Simple array response wrapped in ApiResponse
const apiData = response.data as ApiResponse<T[]>;
setData(apiData.data);
setTotal(apiData.data.length);
setHasMore(false);
return apiData.data;
} else if (Array.isArray(response.data)) {
// Direct array response
setData(response.data);
setTotal(response.data.length);
setHasMore(false);
return response.data;
} else {
throw new Error('Invalid response format');
}
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || 'Failed to fetch data';
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
}, [client, endpoint]);
const getById = useCallback(async (id: string): Promise<T> => {
setLoading(true);
setError(null);
try {
const response = await client.get<ApiResponse<T> | T>(`${endpoint}/${id}`);
const item = 'data' in response.data ? response.data.data : response.data;
return item;
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || 'Failed to fetch item';
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
}, [client, endpoint]);
const create = useCallback(async (itemData: Partial<T>): Promise<T> => {
setLoading(true);
setError(null);
try {
const response = await client.post<ApiResponse<T> | T>(endpoint, itemData);
const newItem = 'data' in response.data ? response.data.data : response.data;
// Update local data
setData(prev => [...prev, newItem]);
setTotal(prev => prev + 1);
return newItem;
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || 'Failed to create item';
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
}, [client, endpoint]);
const update = useCallback(async (id: string, itemData: Partial<T>): Promise<T> => {
setLoading(true);
setError(null);
try {
const response = await client.put<ApiResponse<T> | T>(`${endpoint}/${id}`, itemData);
const updatedItem = 'data' in response.data ? response.data.data : response.data;
// Update local data
setData(prev => prev.map(item => item.id === id ? updatedItem : item));
return updatedItem;
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || 'Failed to update item';
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
}, [client, endpoint]);
const deleteItem = useCallback(async (id: string): Promise<void> => {
setLoading(true);
setError(null);
try {
await client.delete(`${endpoint}/${id}`);
// Update local data
setData(prev => prev.filter(item => item.id !== id));
setTotal(prev => prev - 1);
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || 'Failed to delete item';
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
}, [client, endpoint]);
const refresh = useCallback(async () => {
await getAll();
}, [getAll]);
return {
data,
loading,
error,
total,
hasMore,
client,
getAll,
getById,
create,
update,
delete: deleteItem,
clearError,
refresh,
};
};