- Enable SSR for match pages by detecting server vs client context in API client - Fix 500 errors on economy, chat, and details tabs by adding data loaders - Handle unparsed matches gracefully with "Match Not Parsed" messages - Fix dynamic team ID detection instead of hardcoding team IDs 2/3 - Fix DataTable component to properly render HTML in render functions - Add fixed column widths to tables for visual consistency - Add meta titles to all tab page loaders - Fix Svelte 5 $derived syntax errors - Fix ESLint errors (unused imports, any types, reactive state) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
194 lines
4.8 KiB
TypeScript
194 lines
4.8 KiB
TypeScript
import axios from 'axios';
|
|
import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
|
|
import { APIException } from '$lib/types';
|
|
|
|
/**
|
|
* API Client Configuration
|
|
*/
|
|
const getAPIBaseURL = (): string => {
|
|
const apiUrl = import.meta.env?.VITE_API_BASE_URL || 'https://api.csgow.tf';
|
|
|
|
// Check if we're running on the server (SSR) or in production
|
|
// On the server, we must use the actual API URL, not the proxy
|
|
if (import.meta.env.SSR || import.meta.env.PROD) {
|
|
return apiUrl;
|
|
}
|
|
|
|
// In development mode on the client, use the Vite proxy to avoid CORS issues
|
|
// The proxy will forward /api requests to VITE_API_BASE_URL
|
|
return '/api';
|
|
};
|
|
|
|
const API_BASE_URL = getAPIBaseURL();
|
|
const API_TIMEOUT = Number(import.meta.env?.VITE_API_TIMEOUT) || 10000;
|
|
|
|
// Log the API configuration
|
|
if (import.meta.env.DEV) {
|
|
if (import.meta.env.SSR) {
|
|
console.log('[API Client] SSR mode - using direct API URL:', API_BASE_URL);
|
|
} else {
|
|
console.log('[API Client] Browser mode - using Vite proxy');
|
|
console.log('[API Client] Frontend requests: /api/*');
|
|
console.log(
|
|
'[API Client] Proxy target:',
|
|
import.meta.env?.VITE_API_BASE_URL || 'https://api.csgow.tf'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base API Client
|
|
* Provides centralized HTTP communication with error handling
|
|
*/
|
|
class APIClient {
|
|
private client: AxiosInstance;
|
|
private abortControllers: Map<string, AbortController>;
|
|
|
|
constructor() {
|
|
this.client = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
timeout: API_TIMEOUT,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json'
|
|
}
|
|
});
|
|
|
|
this.abortControllers = new Map();
|
|
|
|
// Request interceptor
|
|
this.client.interceptors.request.use(
|
|
(config) => {
|
|
// Add request ID for tracking
|
|
const requestId = `${config.method}_${config.url}_${Date.now()}`;
|
|
config.headers['X-Request-ID'] = requestId;
|
|
return config;
|
|
},
|
|
(error) => Promise.reject(error)
|
|
);
|
|
|
|
// Response interceptor for error handling
|
|
this.client.interceptors.response.use(
|
|
(response) => response,
|
|
(error: AxiosError) => {
|
|
const apiError = this.handleError(error);
|
|
return Promise.reject(apiError);
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle API errors and convert to APIException
|
|
*/
|
|
private handleError(error: AxiosError): APIException {
|
|
// Network error (no response from server)
|
|
if (!error.response) {
|
|
if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
|
|
return APIException.timeout('Request timed out. Please try again.');
|
|
}
|
|
return APIException.networkError(
|
|
'Unable to connect to the server. Please check your internet connection.'
|
|
);
|
|
}
|
|
|
|
// Server responded with error status
|
|
const { status, data } = error.response;
|
|
return APIException.fromResponse(status, data);
|
|
}
|
|
|
|
/**
|
|
* GET request
|
|
*/
|
|
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
const response = await this.client.get<T>(url, config);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* POST request
|
|
*/
|
|
async post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
const response = await this.client.post<T>(url, data, config);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* PUT request
|
|
*/
|
|
async put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
const response = await this.client.put<T>(url, data, config);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* DELETE request
|
|
*/
|
|
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
const response = await this.client.delete<T>(url, config);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Cancelable GET request
|
|
* Automatically cancels previous request with same key
|
|
*/
|
|
async getCancelable<T>(url: string, key: string, config?: AxiosRequestConfig): Promise<T> {
|
|
// Cancel previous request with same key
|
|
if (this.abortControllers.has(key)) {
|
|
this.abortControllers.get(key)?.abort();
|
|
}
|
|
|
|
// Create new abort controller
|
|
const controller = new AbortController();
|
|
this.abortControllers.set(key, controller);
|
|
|
|
try {
|
|
const response = await this.client.get<T>(url, {
|
|
...config,
|
|
signal: controller.signal
|
|
});
|
|
this.abortControllers.delete(key);
|
|
return response.data;
|
|
} catch (error) {
|
|
this.abortControllers.delete(key);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancel a specific request by key
|
|
*/
|
|
cancelRequest(key: string): void {
|
|
const controller = this.abortControllers.get(key);
|
|
if (controller) {
|
|
controller.abort();
|
|
this.abortControllers.delete(key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancel all pending requests
|
|
*/
|
|
cancelAllRequests(): void {
|
|
this.abortControllers.forEach((controller) => controller.abort());
|
|
this.abortControllers.clear();
|
|
}
|
|
|
|
/**
|
|
* Get base URL for constructing full URLs
|
|
*/
|
|
getBaseURL(): string {
|
|
return API_BASE_URL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Singleton API client instance
|
|
*/
|
|
export const apiClient = new APIClient();
|
|
|
|
/**
|
|
* Export for testing/mocking
|
|
*/
|
|
export { APIClient };
|