forked from 0xWheatyz/SPARC
feat(frontend): add React dashboard with TypeScript
Add modern React frontend to replace Streamlit dashboard: - Vite build system with TypeScript - Tailwind CSS for styling - Component structure in src/ - Production Dockerfile with nginx - Development server on port 5173 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||
import type { TokenResponse, User, CompanyAnalysis, BatchAnalysisResult, JobStatus, Analytics } from '../types';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Token management
|
||||
let accessToken: string | null = localStorage.getItem('access_token');
|
||||
let refreshToken: string | null = localStorage.getItem('refresh_token');
|
||||
|
||||
export const setTokens = (tokens: TokenResponse) => {
|
||||
accessToken = tokens.access_token;
|
||||
refreshToken = tokens.refresh_token;
|
||||
localStorage.setItem('access_token', tokens.access_token);
|
||||
localStorage.setItem('refresh_token', tokens.refresh_token);
|
||||
};
|
||||
|
||||
export const clearTokens = () => {
|
||||
accessToken = null;
|
||||
refreshToken = null;
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
};
|
||||
|
||||
export const getAccessToken = () => accessToken;
|
||||
|
||||
// Request interceptor to add auth header
|
||||
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
if (accessToken) {
|
||||
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Response interceptor to handle token refresh
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
|
||||
|
||||
if (error.response?.status === 401 && !originalRequest._retry && refreshToken) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
try {
|
||||
const response = await axios.post<TokenResponse>(`${API_BASE_URL}/auth/refresh`, {
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
|
||||
setTokens(response.data);
|
||||
originalRequest.headers.Authorization = `Bearer ${response.data.access_token}`;
|
||||
|
||||
return api(originalRequest);
|
||||
} catch {
|
||||
clearTokens();
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Auth API
|
||||
export const authApi = {
|
||||
register: async (email: string, password: string): Promise<User> => {
|
||||
const response = await api.post<User>('/auth/register', { email, password });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
login: async (email: string, password: string): Promise<TokenResponse> => {
|
||||
const response = await api.post<TokenResponse>('/auth/login', { email, password });
|
||||
setTokens(response.data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getMe: async (): Promise<User> => {
|
||||
const response = await api.get<User>('/auth/me');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
clearTokens();
|
||||
},
|
||||
};
|
||||
|
||||
// Analysis API
|
||||
export const analysisApi = {
|
||||
analyzeCompany: async (companyName: string): Promise<CompanyAnalysis> => {
|
||||
const response = await api.get<CompanyAnalysis>(`/analyze/${encodeURIComponent(companyName)}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
analyzeBatch: async (companies: string[], maxWorkers = 3): Promise<BatchAnalysisResult> => {
|
||||
const response = await api.post<BatchAnalysisResult>('/analyze/batch', {
|
||||
companies,
|
||||
max_workers: maxWorkers,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
analyzeBatchAsync: async (companies: string[], maxWorkers = 3): Promise<JobStatus> => {
|
||||
const response = await api.post<JobStatus>('/analyze/batch/async', {
|
||||
companies,
|
||||
max_workers: maxWorkers,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getJobStatus: async (jobId: string): Promise<JobStatus> => {
|
||||
const response = await api.get<JobStatus>(`/jobs/${jobId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
listJobs: async (status?: string, limit = 10): Promise<JobStatus[]> => {
|
||||
const params = new URLSearchParams();
|
||||
if (status) params.append('status', status);
|
||||
params.append('limit', limit.toString());
|
||||
const response = await api.get<JobStatus[]>(`/jobs?${params}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// Analytics API
|
||||
export const analyticsApi = {
|
||||
getAnalytics: async (days = 30): Promise<Analytics> => {
|
||||
const response = await api.get<Analytics>(`/analytics?days=${days}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// Admin API
|
||||
export const adminApi = {
|
||||
listUsers: async (limit = 100, offset = 0): Promise<User[]> => {
|
||||
const response = await api.get<User[]>(`/admin/users?limit=${limit}&offset=${offset}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateUserRole: async (userId: number, role: 'admin' | 'user'): Promise<User> => {
|
||||
const response = await api.patch<User>(`/admin/users/${userId}/role`, { role });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteUser: async (userId: number): Promise<void> => {
|
||||
await api.delete(`/admin/users/${userId}`);
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
Reference in New Issue
Block a user