project init

This commit is contained in:
2026-02-06 02:17:59 +03:00
commit b9d9b9ed17
129 changed files with 22835 additions and 0 deletions

240
apps/web/src/lib/api.ts Normal file
View File

@ -0,0 +1,240 @@
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
const API_URL = `${API_BASE}/api`;
const STORAGE_KEY = 'coursecraft_api_token';
/** JWT from backend (set after exchangeToken). API expects JWT in Authorization header. */
let apiToken: string | null = null;
function getStoredToken(): string | null {
if (typeof window === 'undefined') return null;
try {
return sessionStorage.getItem(STORAGE_KEY);
} catch {
return null;
}
}
export function setApiToken(token: string | null) {
apiToken = token;
try {
if (typeof window !== 'undefined') {
if (token) sessionStorage.setItem(STORAGE_KEY, token);
else sessionStorage.removeItem(STORAGE_KEY);
}
} catch {}
}
/** Returns current JWT (memory or sessionStorage after reload). */
function getApiToken(): string | null {
if (apiToken) return apiToken;
const stored = getStoredToken();
if (stored) {
apiToken = stored;
return stored;
}
return null;
}
class ApiClient {
private getAuthHeaders(): HeadersInit {
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
const token = getApiToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const headers = this.getAuthHeaders();
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers: {
...headers,
...options.headers,
},
});
if (!response.ok) {
if (response.status === 401) {
setApiToken(null);
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('auth:unauthorized'));
}
}
const error = await response.json().catch(() => ({ message: 'Request failed' }));
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
// Auth - Exchange Supabase token for internal JWT (no Authorization header)
async exchangeToken(supabaseToken: string) {
return this.request<{ accessToken: string; user: any }>('/auth/exchange', {
method: 'POST',
body: JSON.stringify({ supabaseToken }),
});
}
// User
async getProfile() {
return this.request<any>('/users/me');
}
async updateProfile(data: { name?: string; bio?: string }) {
return this.request<any>('/users/me', {
method: 'PATCH',
body: JSON.stringify(data),
});
}
async getSettings() {
return this.request<any>('/users/me/settings');
}
async updateSettings(data: { customAiModel?: string; language?: string; theme?: string }) {
return this.request<any>('/users/me/settings', {
method: 'PATCH',
body: JSON.stringify(data),
});
}
// Courses
async getCourses(params?: { status?: string; page?: number; limit?: number }) {
const searchParams = new URLSearchParams();
if (params?.status) searchParams.set('status', params.status);
if (params?.page) searchParams.set('page', String(params.page));
if (params?.limit) searchParams.set('limit', String(params.limit));
const query = searchParams.toString();
return this.request<{ data: any[]; meta: any }>(`/courses${query ? `?${query}` : ''}`);
}
async getCourse(id: string) {
return this.request<any>(`/courses/${id}`);
}
async createCourse(data: { title: string; description?: string }) {
return this.request<any>('/courses', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateCourse(id: string, data: any) {
return this.request<any>(`/courses/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
}
async deleteCourse(id: string) {
return this.request<void>(`/courses/${id}`, {
method: 'DELETE',
});
}
// Chapters
async createChapter(courseId: string, data: { title: string; description?: string }) {
return this.request<any>(`/courses/${courseId}/chapters`, {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateChapter(courseId: string, chapterId: string, data: any) {
return this.request<any>(`/courses/${courseId}/chapters/${chapterId}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
}
async deleteChapter(courseId: string, chapterId: string) {
return this.request<void>(`/courses/${courseId}/chapters/${chapterId}`, {
method: 'DELETE',
});
}
// Lessons (API: GET/PATCH /courses/:courseId/lessons/:lessonId)
async getLesson(courseId: string, lessonId: string) {
return this.request<any>(`/courses/${courseId}/lessons/${lessonId}`);
}
async updateLesson(courseId: string, lessonId: string, data: any) {
return this.request<any>(`/courses/${courseId}/lessons/${lessonId}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
}
// Generation
async startGeneration(prompt: string) {
return this.request<{ id: string; status: string; progress: number }>('/generation/start', {
method: 'POST',
body: JSON.stringify({ prompt }),
});
}
async getGenerationStatus(id: string) {
return this.request<{
id: string;
status: string;
progress: number;
currentStep: string | null;
questions: any[] | null;
generatedOutline: any | null;
errorMessage: string | null;
course: { id: string; slug: string } | null;
}>(`/generation/${id}/status`);
}
async answerQuestions(id: string, answers: Record<string, string | string[]>) {
return this.request<{ success: boolean }>(`/generation/${id}/answer`, {
method: 'POST',
body: JSON.stringify({ answers }),
});
}
async cancelGeneration(id: string) {
return this.request<{ success: boolean }>(`/generation/${id}/cancel`, {
method: 'POST',
});
}
// Subscription
async getSubscription() {
return this.request<any>('/payments/subscription');
}
async createCheckoutSession(priceId: string) {
return this.request<{ url: string }>('/payments/create-checkout-session', {
method: 'POST',
body: JSON.stringify({ priceId }),
});
}
async createPortalSession() {
return this.request<{ url: string }>('/payments/create-portal-session', {
method: 'POST',
});
}
// Search
async searchCourses(query: string, filters?: { category?: string; difficulty?: string }) {
const searchParams = new URLSearchParams({ q: query });
if (filters?.category) searchParams.set('category', filters.category);
if (filters?.difficulty) searchParams.set('difficulty', filters.difficulty);
return this.request<{ hits: any[]; totalHits: number }>(`/search?${searchParams}`);
}
}
export const api = new ApiClient();