Implemented complete design system and foundational infrastructure: **Design System Components:** - Button (all variants: primary, secondary, ghost, danger) - Input & Textarea (with validation and error states) - Card (elevated, outlined, flat variants) - Modal/Dialog (with focus trap and accessibility) - Avatar (with fallback initials) - Badge (all color variants) - Form helpers (FormField, Checkbox, Select) - Link component with Next.js integration - Navigation (Header, Footer with responsive design) **Layouts:** - MainLayout (with Header/Footer for public pages) - AuthLayout (minimal layout for auth flows) - DashboardLayout (with sidebar navigation) **Hooks & Utilities:** - useAuth() - authentication state management - useApi() - API calls with loading/error states - useLocalStorage() - persistent state management - apiClient - Axios instance with token refresh - authStore - Zustand store for auth state **Configuration:** - Tailwind config with design tokens - Dark mode support via CSS variables - Global styles with accessibility focus - WCAG 2.2 AA+ compliant focus indicators All components follow accessibility best practices with proper ARIA labels, keyboard navigation, and screen reader support. Job ID: MTAD-IMPL-2025-11-18-CL
77 lines
2.2 KiB
TypeScript
77 lines
2.2 KiB
TypeScript
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'
|
|
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api/v1'
|
|
|
|
// Create axios instance
|
|
export const apiClient: AxiosInstance = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
timeout: 30000, // 30 seconds
|
|
})
|
|
|
|
// Request interceptor to add auth token
|
|
apiClient.interceptors.request.use(
|
|
(config) => {
|
|
// Get token from localStorage (will be replaced with proper auth context)
|
|
if (typeof window !== 'undefined') {
|
|
const token = localStorage.getItem('access_token')
|
|
if (token && config.headers) {
|
|
config.headers.Authorization = `Bearer ${token}`
|
|
}
|
|
}
|
|
return config
|
|
},
|
|
(error) => {
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
|
|
// Response interceptor to handle token refresh
|
|
apiClient.interceptors.response.use(
|
|
(response) => response,
|
|
async (error: AxiosError) => {
|
|
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean }
|
|
|
|
// If 401 and we haven't retried yet, try to refresh token
|
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
originalRequest._retry = true
|
|
|
|
try {
|
|
const refreshToken = localStorage.getItem('refresh_token')
|
|
if (!refreshToken) {
|
|
throw new Error('No refresh token available')
|
|
}
|
|
|
|
const response = await axios.post(`${API_BASE_URL}/auth/refresh`, {
|
|
refresh_token: refreshToken,
|
|
})
|
|
|
|
const { access_token } = response.data
|
|
localStorage.setItem('access_token', access_token)
|
|
|
|
// Retry original request with new token
|
|
if (originalRequest.headers) {
|
|
originalRequest.headers.Authorization = `Bearer ${access_token}`
|
|
}
|
|
|
|
return apiClient(originalRequest)
|
|
} catch (refreshError) {
|
|
// Refresh failed, clear tokens and redirect to login
|
|
localStorage.removeItem('access_token')
|
|
localStorage.removeItem('refresh_token')
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.location.href = '/auth/login'
|
|
}
|
|
|
|
return Promise.reject(refreshError)
|
|
}
|
|
}
|
|
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
|
|
export default apiClient
|