const API_URL = import.meta.env.VITE_API_URL || '/api/v1'; const SESSION_KEY = 'compor_hub_session'; export type ApiRole = 'student' | 'instructor' | 'admin'; export interface ApiUser { id: string; email: string; name: string; role: ApiRole; } export interface Profile { id: string; email?: string; name: string; role: ApiRole; avatarImageUrl: string | null; headline: string; bio: string; websiteUrl: string | null; linkedinUrl: string | null; instagramUrl: string | null; youtubeUrl: string | null; isPublic: boolean; } export type ProfileInput = Omit; export interface ManagedUser { id: string; email: string; name: string; role: ApiRole; isActive: boolean; createdAt: string; } export interface ManagedUserDetail extends ManagedUser { completedLessons: number; lastLearningAt: string | null; } export interface PlatformAnalytics { totalUsers: number; activeUsers: number; publishedCourses: number; completedLessons: number; comments: number; } export interface InstructorAnalytics { courses: number; lessons: number; learners: number; completedLessons: number; comments: number; } export interface AuditEntry { id: string; action: string; subjectType: string; subjectId: string | null; metadata: Record; createdAt: string; actorName: string; } export interface BunnyVideo { id: string; title: string; status: 'processing' | 'ready' | 'failed'; providerStatus: number; encodeProgress: number; durationSeconds: number | null; } export interface LessonPlayback { provider: string; kind: 'embed' | 'video'; embedUrl?: string; source?: string; expiresAt?: string; } export interface LearningPathCourse { id: string; title: string; status: 'draft' | 'published' | 'archived'; coverImageUrl: string | null; position: number; } export interface LearningPath { id: string; title: string; description: string; coverImageUrl: string | null; coverImageZoom: number; coverImagePositionX: number; coverImagePositionY: number; status: 'draft' | 'published' | 'archived'; publishedAt: string | null; createdAt: string; courses: LearningPathCourse[]; } export interface LearningPathInput { title: string; description: string; coverImageUrl: string | null; coverImageZoom: number; coverImagePositionX: number; coverImagePositionY: number; status: 'draft' | 'published'; courseIds: string[]; } export interface HomeBannerInput { kind: 'course' | 'custom'; courseId: string | null; imageUrl: string | null; title: string; description: string; } export interface HomeBanner extends HomeBannerInput { id: string; position: number; courseTitle: string | null; courseDescription: string | null; courseImageUrl: string | null; } interface Session { token: string; user: ApiUser; } export class ApiError extends Error { constructor(message: string, public status: number) { super(message); } } export function getSession(): Session | null { const value = localStorage.getItem(SESSION_KEY); if (!value) return null; try { return JSON.parse(value) as Session; } catch { localStorage.removeItem(SESSION_KEY); return null; } } export function saveSession(session: Session) { localStorage.setItem(SESSION_KEY, JSON.stringify(session)); } export function clearSession() { localStorage.removeItem(SESSION_KEY); } export async function apiRequest(path: string, options: RequestInit = {}): Promise { const session = getSession(); const headers = new Headers(options.headers); if (options.body) headers.set('Content-Type', 'application/json'); if (session) headers.set('Authorization', `Bearer ${session.token}`); const response = await fetch(`${API_URL}${path}`, { ...options, headers }); if (response.status === 204) return undefined as T; const body = await response.json().catch(() => ({})); if (!response.ok) { throw new ApiError(body.error || 'The request could not be completed', response.status); } return body as T; } async function uploadRequest(path: string, file: File): Promise { const session = getSession(); const headers = new Headers({ 'Content-Type': file.type || 'application/octet-stream' }); if (session) headers.set('Authorization', `Bearer ${session.token}`); const response = await fetch(`${API_URL}${path}`, { method: 'PUT', headers, body: file }); const body = await response.json().catch(() => ({})); if (!response.ok) throw new ApiError(body.error || 'The video upload could not be completed', response.status); return body as T; } async function uploadImageRequest(path: string, file: File): Promise { const session = getSession(); const headers = new Headers({ 'Content-Type': file.type || 'application/octet-stream' }); if (session) headers.set('Authorization', `Bearer ${session.token}`); const response = await fetch(`${API_URL}${path}`, { method: 'POST', headers, body: file }); const body = await response.json().catch(() => ({})); if (!response.ok) throw new ApiError(body.error || 'The cover image could not be uploaded', response.status); return body as T; } export const authApi = { async login(email: string, password: string) { return apiRequest('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }); }, async register(name: string, email: string, password: string) { return apiRequest('/auth/register', { method: 'POST', body: JSON.stringify({ name, email, password }) }); }, async me() { return apiRequest<{ user: ApiUser }>('/auth/me'); }, async acceptInvitation(token: string, name: string, password: string) { return apiRequest('/auth/accept-invitation', { method: 'POST', body: JSON.stringify({ token, name, password }) }); }, async resetPassword(token: string, password: string) { return apiRequest('/auth/reset-password', { method: 'POST', body: JSON.stringify({ token, password }) }); }, }; export const adminApi = { async dashboard() { return apiRequest<{ data: PlatformAnalytics }>('/admin/dashboard'); }, async listUsers() { return apiRequest<{ data: ManagedUser[] }>('/admin/users'); }, async updateUser(userId: string, update: Partial>) { return apiRequest<{ data: ManagedUser }>(`/admin/users/${userId}`, { method: 'PATCH', body: JSON.stringify(update), }); }, async invite(email: string, role: 'instructor' = 'instructor') { return apiRequest<{ data: { inviteUrl: string } }>('/admin/invitations', { method: 'POST', body: JSON.stringify({ email, role }) }); }, async passwordReset(userId: string) { return apiRequest<{ data: { resetUrl: string } }>(`/admin/users/${userId}/password-reset`, { method: 'POST' }); }, async deleteUser(userId: string) { return apiRequest(`/admin/users/${userId}`, { method: 'DELETE' }); }, async userDetail(userId: string) { return apiRequest<{ data: ManagedUserDetail }>(`/admin/users/${userId}`); }, async auditLog() { return apiRequest<{ data: AuditEntry[] }>('/admin/audit-log'); }, async categories() { return apiRequest<{ data: Array<{ id: string; name: string; position: number; isActive: boolean }> }>('/admin/categories'); }, async createCategory(name: string) { return apiRequest<{ data: { id: string; name: string; position: number; isActive: boolean } }>('/admin/categories', { method: 'POST', body: JSON.stringify({ name }) }); }, async updateCategory(categoryId: string, update: { name: string; isActive?: boolean; position?: number }) { return apiRequest<{ data: { id: string; name: string; position: number; isActive: boolean } }>(`/admin/categories/${categoryId}`, { method: 'PATCH', body: JSON.stringify(update) }); }, async listPaths() { return apiRequest<{ data: LearningPath[] }>('/admin/paths'); }, async createPath(input: LearningPathInput) { return apiRequest<{ data: LearningPath }>('/admin/paths', { method: 'POST', body: JSON.stringify(input) }); }, async updatePath(pathId: string, input: LearningPathInput) { return apiRequest<{ data: LearningPath }>(`/admin/paths/${pathId}`, { method: 'PATCH', body: JSON.stringify(input) }); }, async deletePath(pathId: string) { return apiRequest(`/admin/paths/${pathId}`, { method: 'DELETE' }); }, async homeBanners() { return apiRequest<{ data: HomeBanner[] }>('/admin/home-banners'); }, async updateHomeBanners(banners: HomeBannerInput[]) { return apiRequest<{ data: HomeBanner[] }>('/admin/home-banners', { method: 'PUT', body: JSON.stringify({ banners }) }); }, async homeConfiguration() { return apiRequest<{ data: { featuredCourseId: string | null; courseOrder: string[]; defaultCoverImageUrl: string | null; courses: Array<{ id: string; title: string; status: string; category: string; coverImageUrl: string | null }> } }>('/admin/home-configuration'); }, async updateHomeConfiguration(input: { featuredCourseId: string | null; courseOrder: string[]; defaultCoverImageUrl: string | null }) { return apiRequest<{ data: typeof input }>('/admin/home-configuration', { method: 'PUT', body: JSON.stringify(input) }); }, }; export const profileApi = { async me() { return apiRequest<{ data: Profile }>('/profiles/me'); }, async update(input: ProfileInput) { return apiRequest<{ data: Profile }>('/profiles/me', { method: 'PATCH', body: JSON.stringify(input) }); }, async instructor(instructorId: string) { return apiRequest<{ data: Profile & { courses: unknown[] } }>(`/profiles/instructors/${instructorId}`); }, }; export const instructorApi = { async analytics() { return apiRequest<{ data: InstructorAnalytics }>('/manage/courses/analytics'); }, async bunnyConfiguration() { return apiRequest<{ data: { configured: boolean; maxUploadBytes: number } }>('/manage/media/bunny/config'); }, async createBunnyVideo(title: string) { return apiRequest<{ data: BunnyVideo }>('/manage/media/bunny/videos', { method: 'POST', body: JSON.stringify({ title }) }); }, async uploadBunnyVideo(videoId: string, file: File) { return uploadRequest<{ data: BunnyVideo }>(`/manage/media/bunny/videos/${videoId}/upload`, file); }, async bunnyVideoStatus(videoId: string) { return apiRequest<{ data: BunnyVideo }>(`/manage/media/bunny/videos/${videoId}`); }, async deleteBunnyVideo(videoId: string) { return apiRequest(`/manage/media/bunny/videos/${videoId}`, { method: 'DELETE' }); }, async coverConfiguration() { return apiRequest<{ data: { configured: boolean; maxUploadBytes: number } }>('/manage/media/covers/config'); }, async uploadCover(file: File) { return uploadImageRequest<{ data: { key: string; coverImageUrl: string } }>('/manage/media/covers', file); }, }; export const courseApi = { async categories() { return apiRequest<{ data: string[] }>('/courses/categories'); }, async playback(courseId: string, lessonId: string) { return apiRequest<{ data: LessonPlayback }>(`/courses/${courseId}/lessons/${lessonId}/playback`); }, };