123 lines
3.7 KiB
TypeScript
123 lines
3.7 KiB
TypeScript
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://api.approagency.ir/api';
|
|
|
|
interface ApiOptions extends RequestInit {
|
|
token?: string;
|
|
}
|
|
|
|
// Simple in-memory cache for ongoing requests
|
|
const pendingRequests = new Map();
|
|
|
|
class ApiClient {
|
|
private async request<T>(
|
|
endpoint: string,
|
|
options: ApiOptions = {}
|
|
): Promise<T> {
|
|
const { token, ...fetchOptions } = options;
|
|
|
|
const headers: Record<string, string> = {
|
|
// Always ask for JSON so Laravel returns 401 JSON on auth failure
|
|
// instead of a 302 redirect to the login page.
|
|
Accept: 'application/json',
|
|
...(options.headers as Record<string, string> || {}),
|
|
};
|
|
|
|
// Don't set Content-Type for FormData (browser will set it automatically with boundary)
|
|
if (!(options.body instanceof FormData)) {
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
// Create a unique key for this request
|
|
const requestKey = `${fetchOptions.method || 'GET'}-${endpoint}-${JSON.stringify(options.body)}`;
|
|
|
|
// Check if there's already a pending request with the same key
|
|
if (pendingRequests.has(requestKey)) {
|
|
return pendingRequests.get(requestKey);
|
|
}
|
|
|
|
// Make the request
|
|
const promise = fetch(`${API_BASE_URL}${endpoint}`, {
|
|
...fetchOptions,
|
|
headers,
|
|
}).then(async (response) => {
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.message || 'API request failed');
|
|
}
|
|
return response.json();
|
|
}).finally(() => {
|
|
// Clean up after request completes
|
|
pendingRequests.delete(requestKey);
|
|
});
|
|
|
|
// Store the promise
|
|
pendingRequests.set(requestKey, promise);
|
|
|
|
return promise;
|
|
}
|
|
|
|
async post<T>(endpoint: string, data: any, token?: string): Promise<T> {
|
|
const body = data instanceof FormData ? data : JSON.stringify(data);
|
|
|
|
return this.request<T>(endpoint, {
|
|
method: 'POST',
|
|
body,
|
|
token,
|
|
});
|
|
}
|
|
|
|
async get<T>(endpoint: string, token?: string): Promise<T> {
|
|
return this.request<T>(endpoint, {
|
|
method: 'GET',
|
|
token,
|
|
});
|
|
}
|
|
|
|
async put<T>(endpoint: string, data: any, token?: string): Promise<T> {
|
|
const body = data instanceof FormData ? data : JSON.stringify(data);
|
|
|
|
return this.request<T>(endpoint, {
|
|
method: 'PUT',
|
|
body,
|
|
token,
|
|
});
|
|
}
|
|
|
|
async patch<T>(endpoint: string, data: any, token?: string): Promise<T> {
|
|
const body = data instanceof FormData ? data : JSON.stringify(data);
|
|
|
|
return this.request<T>(endpoint, {
|
|
method: 'PATCH',
|
|
body,
|
|
token,
|
|
});
|
|
}
|
|
|
|
async delete<T>(endpoint: string, data?: any, token?: string): Promise<T> {
|
|
const options: ApiOptions = {
|
|
method: 'DELETE',
|
|
token,
|
|
};
|
|
|
|
if (data) {
|
|
options.body = data instanceof FormData ? data : JSON.stringify(data);
|
|
|
|
if (!(data instanceof FormData)) {
|
|
options.headers = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
}
|
|
}
|
|
|
|
return this.request<T>(endpoint, options);
|
|
}
|
|
|
|
async deleteWithBody<T>(endpoint: string, data: any, token?: string): Promise<T> {
|
|
return this.delete<T>(endpoint, data, token);
|
|
}
|
|
}
|
|
|
|
export const apiClient = new ApiClient(); |