import { isEnvDemoMode } from '@/lib/demo-mode'; import { getApiV1BaseUrl } from '@/lib/api-url'; const API_BASE_URL = getApiV1BaseUrl(); // Demo mode is controlled by environment only. const isDemoMode = () => { return isEnvDemoMode(); }; // Helper function to get auth headers const getAuthHeaders = () => { const token = localStorage.getItem('token'); return { 'Content-Type': 'application/json', ...(token && { 'Authorization': `Bearer ${token}` }), }; }; // Generic API client class ApiClient { private baseURL: string; constructor(baseURL: string) { this.baseURL = baseURL; } private async request( endpoint: string, options: RequestInit = {} ): Promise { // If in demo mode, use mock data if (isDemoMode()) { return this.getMockResponse(endpoint, options); } const url = `${this.baseURL}${endpoint}`; const config: RequestInit = { headers: { ...getAuthHeaders(), ...options.headers, }, ...options, }; try { const response = await fetch(url, config); if (!response.ok) { const message = await response.text(); throw new Error(message || `HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error(`API request failed for ${endpoint}:`, error); throw error; } } private async getMockResponse(endpoint: string, options: RequestInit): Promise { // Import mock data dynamically to avoid circular dependencies const { getMockStats, getMockDocuments, getMockBookmarks, getMockTasks, getMockNotes, getMockTimeEntries, getMockLearningPaths, getMockVideos } = await import('./mockData'); const method = options.method || 'GET'; // Dashboard stats if (endpoint.includes('/dashboard/stats')) { return getMockStats() as T; } // Documents/Files if (endpoint.includes('/documents') || endpoint.includes('/files')) { if (method === 'GET') { return getMockDocuments() as T; } } // Bookmarks if (endpoint.includes('/bookmarks')) { if (method === 'GET') { return getMockBookmarks() as T; } } // Tasks if (endpoint.includes('/tasks')) { if (method === 'GET') { return getMockTasks() as T; } } // Notes if (endpoint.includes('/notes')) { if (method === 'GET') { return getMockNotes() as T; } } // Time entries if (endpoint.includes('/time-entries')) { if (method === 'GET') { const mockEntries = getMockTimeEntries(); // Convert mock entries to TimeEntry format const timeEntries = mockEntries.map(entry => ({ id: parseInt(entry.id.replace('time_', '')), user_id: 1, task_id: entry.taskId ? parseInt(entry.taskId.replace('task_', '')) : undefined, start_time: `${entry.date}T${entry.startTime}:00Z`, end_time: entry.endTime ? `${entry.date}T${entry.endTime}:00Z` : undefined, duration: entry.duration, description: entry.description, tags: entry.tags, billable: entry.billable, hourly_rate: entry.hourlyRate, is_running: false, source: 'demo', created_at: `${entry.date}T${entry.startTime}:00Z`, updated_at: entry.endTime ? `${entry.date}T${entry.endTime}:00Z` : `${entry.date}T${entry.startTime}:00Z` })); return { time_entries: timeEntries } as T; } if (method === 'POST') { const mockEntries = getMockTimeEntries(); const entry = mockEntries[0]; // Convert mock entry to TimeEntry format const timeEntry = { id: parseInt(entry.id.replace('time_', '')), user_id: 1, task_id: entry.taskId ? parseInt(entry.taskId.replace('task_', '')) : undefined, start_time: `${entry.date}T${entry.startTime}:00Z`, end_time: entry.endTime ? `${entry.date}T${entry.endTime}:00Z` : undefined, duration: entry.duration, description: entry.description, tags: entry.tags, billable: entry.billable, hourly_rate: entry.hourlyRate, is_running: false, source: 'demo', created_at: `${entry.date}T${entry.startTime}:00Z`, updated_at: entry.endTime ? `${entry.date}T${entry.endTime}:00Z` : `${entry.date}T${entry.startTime}:00Z` }; return { time_entry: timeEntry } as T; } } // Auth endpoints if (endpoint.includes('/auth/login-totp')) { return { token: 'demo-token', user: { id: 1, email: 'demo@trackeep.com', name: 'Demo User' } } as T; } // GitHub repos if (endpoint.includes('/github/repos')) { return { repositories: [ { id: 1, name: 'trackeep', full_name: 'tdvorak/trackeep', stars: 245, forks: 43, watchers: 65, language: 'Go' }, { id: 2, name: 'frontend', full_name: 'tdvorak/frontend', stars: 89, forks: 12, watchers: 23, language: 'TypeScript' }, { id: 3, name: 'mobile-app', full_name: 'tdvorak/mobile-app', stars: 34, forks: 8, watchers: 15, language: 'TypeScript' } ], totalStars: 368, totalForks: 63, totalWatchers: 103 } as T; } // Learning paths if (endpoint.includes('/learning-paths/categories')) { return { categories: ['Web Development', 'DevOps', 'Programming', 'Design', 'Business', 'Data Science'] } as T; } if (endpoint.includes('/learning-paths')) { return getMockLearningPaths() as T; } // Chat sessions if (endpoint.includes('/chat/sessions')) { return { sessions: [ { id: '1', title: 'Project Planning', created_at: '2024-01-15T10:00:00Z', updated_at: '2024-01-15T11:30:00Z' }, { id: '2', title: 'Technical Discussion', created_at: '2024-01-14T14:00:00Z', updated_at: '2024-01-14T15:45:00Z' } ] } as T; } // AI providers if (endpoint.includes('/ai/providers')) { return { providers: [ { id: 'longcat', name: 'LongCat AI', enabled: true, models: ['LongCat-Flash-Chat', 'LongCat-Flash-Thinking'] }, { id: 'mistral', name: 'Mistral AI', enabled: false, models: ['mistral-small-latest', 'mistral-large-latest'] }, { id: 'openai', name: 'OpenAI', enabled: false, models: ['gpt-4', 'gpt-3.5-turbo'] } ] } as T; } // YouTube endpoints if (endpoint.includes('/youtube/video-details')) { return getMockVideos()[0] as T; } if (endpoint.includes('/youtube/predefined-channels')) { return { channels: [ { id: 'UC8butISFwT-Wy7pm24E6Icg', name: 'NetworkChuck', latestVideos: getMockVideos().slice(0, 2) }, { id: 'UCWv7vHwRQdGJtU2i9hJ8X7A', name: 'Fireship', latestVideos: getMockVideos().slice(1, 3) }, { id: 'UCsXVk37bltHxD1rDPgtNG6A', name: 'Beyond Fireship', latestVideos: getMockVideos().slice(0, 1) } ] } as T; } // Default empty response return {} as T; } async get(endpoint: string): Promise { return this.request(endpoint, { method: 'GET' }); } async post(endpoint: string, data?: any): Promise { return this.request(endpoint, { method: 'POST', body: data ? JSON.stringify(data) : undefined, }); } async put(endpoint: string, data?: any): Promise { return this.request(endpoint, { method: 'PUT', body: data ? JSON.stringify(data) : undefined, }); } async delete(endpoint: string): Promise { return this.request(endpoint, { method: 'DELETE' }); } async upload(endpoint: string, formData: FormData): Promise { const url = `${this.baseURL}${endpoint}`; try { const response = await fetch(url, { method: 'POST', headers: { 'Authorization': getAuthHeaders().Authorization || '', }, body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({ error: 'Unknown error' })); throw new Error(error.error || `HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('File upload failed:', error); throw error; } } } const api = new ApiClient(API_BASE_URL); // Types export interface Bookmark { id: number; title: string; url: string; description?: string; tags: string[]; is_public: boolean; created_at: string; updated_at: string; } export interface Task { id: number; title: string; description?: string; status: 'pending' | 'in_progress' | 'completed'; priority: 'low' | 'medium' | 'high'; due_date?: string; tags: string[]; created_at: string; updated_at: string; } export interface Note { id: number; title: string; content?: string; description?: string; tags: string[]; is_public: boolean; created_at: string; updated_at: string; } export interface File { id: number; original_name: string; file_name: string; file_path: string; file_size: number; mime_type: string; file_type: 'document' | 'image' | 'video' | 'audio' | 'archive' | 'other'; description?: string; is_public: boolean; thumbnail_path?: string; preview_path?: string; created_at: string; updated_at: string; } export interface TimeEntry { id: number; user_id: number; task_id?: number; bookmark_id?: number; note_id?: number; start_time: string; end_time?: string; duration?: number; description: string; tags: string[]; billable: boolean; hourly_rate?: number; is_running: boolean; source: string; created_at: string; updated_at: string; task?: Task; bookmark?: Bookmark; note?: Note; } export interface TimeStats { total_time_seconds: number; total_entries: number; running_entries: number; billable_time_seconds: number; total_billable_amount: number; } // API Functions export const bookmarksApi = { getAll: () => api.get('/bookmarks'), getById: (id: number) => api.get(`/bookmarks/${id}`), create: (bookmark: Omit) => api.post('/bookmarks', bookmark), update: (id: number, bookmark: Partial) => api.put(`/bookmarks/${id}`, bookmark), delete: (id: number) => api.delete<{ message: string }>(`/bookmarks/${id}`), }; export const tasksApi = { getAll: () => api.get('/tasks'), getById: (id: number) => api.get(`/tasks/${id}`), create: (task: Omit) => api.post('/tasks', task), update: (id: number, task: Partial) => api.put(`/tasks/${id}`, task), delete: (id: number) => api.delete<{ message: string }>(`/tasks/${id}`), }; export const notesApi = { getAll: (search?: string, tag?: string) => { const params = new URLSearchParams(); if (search) params.append('search', search); if (tag) params.append('tag', tag); const query = params.toString() ? `?${params.toString()}` : ''; return api.get(`/notes${query}`); }, getById: (id: number) => api.get(`/notes/${id}`), create: (note: Omit) => api.post('/notes', note), update: (id: number, note: Partial) => api.put(`/notes/${id}`, note), delete: (id: number) => api.delete<{ message: string }>(`/notes/${id}`), getStats: () => api.get<{ total_notes: number; public_notes: number; private_notes: number; total_tags: number; words_count: number; }>('/notes/stats'), }; export const filesApi = { getAll: () => api.get('/files'), getById: (id: number) => api.get(`/files/${id}`), upload: (file: Blob, description?: string) => { const formData = new FormData(); formData.append('file', file); if (description) formData.append('description', description); return api.upload('/files/upload', formData); }, delete: (id: number) => api.delete<{ message: string }>(`/files/${id}`), download: (id: number) => `${API_BASE_URL}/files/${id}/download`, }; export const timeEntriesApi = { getAll: (startDate?: string, endDate?: string, isRunning?: boolean) => { const params = new URLSearchParams(); if (startDate) params.append('start_date', startDate); if (endDate) params.append('end_date', endDate); if (isRunning !== undefined) params.append('is_running', isRunning.toString()); const query = params.toString() ? `?${params.toString()}` : ''; return api.get<{ time_entries: TimeEntry[] }>(`/time-entries${query}`); }, getById: (id: number) => api.get<{ time_entry: TimeEntry }>(`/time-entries/${id}`), create: (timeEntry: { task_id?: number; bookmark_id?: number; note_id?: number; description: string; tags?: string[]; billable?: boolean; hourly_rate?: number; source?: string; }) => api.post<{ time_entry: TimeEntry }>('/time-entries', timeEntry), update: (id: number, timeEntry: { description?: string; tags?: string[]; billable?: boolean; hourly_rate?: number; end_time?: string; }) => api.put<{ time_entry: TimeEntry }>(`/time-entries/${id}`, timeEntry), stop: (id: number) => api.post<{ time_entry: TimeEntry }>(`/time-entries/${id}/stop`), delete: (id: number) => api.delete<{ message: string }>(`/time-entries/${id}`), getStats: () => api.get<{ stats: TimeStats }>('/time-entries/stats'), }; import { demoBookmarksApi, demoTasksApi, demoNotesApi, demoFilesApi, demoTimeEntriesApi } from './demo-api'; export default api; export { demoBookmarksApi, demoTasksApi, demoNotesApi, demoFilesApi, demoTimeEntriesApi };