239 lines
11 KiB
TypeScript
239 lines
11 KiB
TypeScript
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<Profile, 'id' | 'email' | 'role'>;
|
|
|
|
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<string, unknown>; 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<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
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<T>(path: string, file: File): Promise<T> {
|
|
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<T>(path: string, file: File): Promise<T> {
|
|
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<Session>('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) });
|
|
},
|
|
async register(name: string, email: string, password: string) {
|
|
return apiRequest<Session>('/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<Session>('/auth/accept-invitation', { method: 'POST', body: JSON.stringify({ token, name, password }) });
|
|
},
|
|
async resetPassword(token: string, password: string) {
|
|
return apiRequest<void>('/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<Pick<ManagedUser, 'role' | 'isActive'>>) {
|
|
return apiRequest<{ data: ManagedUser }>(`/admin/users/${userId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(update),
|
|
});
|
|
},
|
|
async invite(email: string, role: 'student' | '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 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<void>(`/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<void>(`/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`);
|
|
},
|
|
};
|