import type * as Types from './types'; import { apiBaseUrl } from '@/lib/site'; import { AUTH_RETURN_TO_STORAGE_KEY, DEFAULT_AUTH_RETURN_TO, getSafeAuthReturnTo } from '@/lib/auth-return'; const API_BASE_URL = apiBaseUrl; type ApiErrorBody = { error?: string; detail?: string; message?: string; }; class ApiClient { private baseUrl: string; private isRefreshing = false; private refreshSubscribers: Array<(token: string) => void> = []; constructor(baseUrl: string) { this.baseUrl = baseUrl; } private getStorageValue(key: string) { if (typeof window === 'undefined') { return null; } return window.localStorage.getItem(key); } private setStorageValue(key: string, value: string) { if (typeof window === 'undefined') { return; } window.localStorage.setItem(key, value); } private removeStorageValue(key: string) { if (typeof window === 'undefined') { return; } window.localStorage.removeItem(key); } private getAuthHeaders(): HeadersInit { const token = this.getStorageValue('access_token'); return { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }; } private async refreshAccessToken(): Promise { const refreshToken = this.getStorageValue('refresh_token'); if (!refreshToken) { throw new Error('No refresh token available'); } const response = await fetch(`${this.baseUrl}/api/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: refreshToken }), }); if (!response.ok) { this.removeStorageValue('access_token'); this.removeStorageValue('refresh_token'); throw new Error('Session expired. Please login again.'); } const data: Types.TokenSchema = await response.json(); this.setStorageValue('access_token', data.access_token); this.setStorageValue('refresh_token', data.refresh_token); return data.access_token; } private onRefreshed(token: string) { this.refreshSubscribers.forEach(callback => callback(token)); this.refreshSubscribers = []; } private addRefreshSubscriber(callback: (token: string) => void) { this.refreshSubscribers.push(callback); } async request( endpoint: string, options: RequestInit = {} ): Promise { const url = `${this.baseUrl}${endpoint}`; const config: RequestInit = { ...options, headers: { ...this.getAuthHeaders(), ...options.headers, }, }; const response = await fetch(url, config); // Handle 401 with automatic token refresh if (response.status === 401 && this.getStorageValue('refresh_token')) { if (!this.isRefreshing) { this.isRefreshing = true; try { const newToken = await this.refreshAccessToken(); this.isRefreshing = false; this.onRefreshed(newToken); // After you obtained `newToken` successfully: const retryConfig: RequestInit = { ...options, headers: { ...(options.headers || {}), Authorization: `Bearer ${newToken}`, // put last so it can't be overwritten }, }; const retryResponse = await fetch(url, retryConfig); if (!retryResponse.ok) { const err = await retryResponse.json().catch(() => ({})); throw new Error(err.error || err.detail || 'Request failed'); } return retryResponse.json(); } catch (error) { this.isRefreshing = false; throw error; } } else { return new Promise((resolve, reject) => { this.addRefreshSubscriber(async (token: string) => { try { const retryConfig: RequestInit = { ...options, headers: { ...(options.headers || {}), Authorization: `Bearer ${token}`, }, }; const retryResponse = await fetch(url, retryConfig); if (!retryResponse.ok) { const err = (await retryResponse.json().catch(() => ({}))) as ApiErrorBody; reject(new Error(err.error || err.detail || 'Request failed after refresh')); } else { resolve(retryResponse.json()); } } catch (e) { reject(e); } }); }); } } if (!response.ok) { const body = (await response.json().catch(() => ({}))) as ApiErrorBody; const message = body?.error || body?.detail || body?.message || 'خطای ناشناخته رخ داد'; throw new Error(message); } return response.json() as Promise; } // ============= Auth Endpoints ============= async register(data: Types.UserRegistrationSchema) { return this.request('/api/auth/register', { method: 'POST', body: JSON.stringify(data), }); } async login(data: Types.UserLoginSchema) { return this.request('/api/auth/login', { method: 'POST', body: JSON.stringify(data), }); } async loginWithOtp(data: Types.UserOtpLoginSchema) { return this.request('/api/auth/login/otp', { method: 'POST', body: JSON.stringify(data), }); } async sendOtp(data: Types.OtpSendSchema) { return this.request('/api/auth/otp/send', { method: 'POST', body: JSON.stringify(data), }); } async verifyRegisterOtp(data: Types.RegisterOtpVerifySchema) { return this.request('/api/auth/otp/verify-register', { method: 'POST', body: JSON.stringify(data), }); } async refreshToken(data: Types.TokenRefreshIn) { return this.request('/api/auth/refresh', { method: 'POST', body: JSON.stringify(data), }); } async sendMobileVerificationOtp(data: Types.MobileOtpSendSchema) { return this.request('/api/auth/mobile/send-otp', { method: 'POST', body: JSON.stringify(data), }); } async verifyMobile(data: Types.MobileOtpVerifySchema) { return this.request('/api/auth/mobile/verify', { method: 'POST', body: JSON.stringify(data), }); } async startGoogleLogin(returnTo = DEFAULT_AUTH_RETURN_TO) { if (typeof window !== 'undefined') { try { window.sessionStorage.setItem(AUTH_RETURN_TO_STORAGE_KEY, getSafeAuthReturnTo(returnTo)); } catch { // The Google flow can still continue if session storage is unavailable. } window.location.href = `${this.baseUrl}/api/auth/oauth/google/start`; } } async getGoogleFlow(flow: string) { return this.request( `/api/auth/oauth/google/flow?flow=${encodeURIComponent(flow)}` ); } async completeGoogleSignup(data: Types.GoogleCompleteSchema) { return this.request('/api/auth/oauth/google/complete', { method: 'POST', body: JSON.stringify(data), }); } async resendGoogleClaimOtp(flow: string) { return this.request('/api/auth/oauth/google/claim/send-otp', { method: 'POST', body: JSON.stringify({ flow }), }); } async verifyGoogleClaim(flow: string, code: string) { return this.request('/api/auth/oauth/google/claim/verify', { method: 'POST', body: JSON.stringify({ flow, code }), }); } async getProfile() { return this.request('/api/auth/profile' ); } async getPublicProfile(username: string) { return this.request( `/api/auth/public-profile/${encodeURIComponent(username)}`, ); } async getMyGamification() { return this.request("/api/gamification/me", { method: "GET", }); } async updateProfile(data: Types.UserUpdateSchema) { return this.request('/api/auth/profile', { method: 'PUT', body: JSON.stringify(data), }); } async uploadProfilePicture(file: File) { const formData = new FormData(); formData.append('file', file); const token = this.getStorageValue('access_token'); const response = await fetch(`${this.baseUrl}/api/auth/profile/picture`, { method: 'POST', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: formData, }); if (!response.ok) { const error: Types.ErrorSchema = await response.json().catch(() => ({ detail: 'خطای آپلود تصویر', })); throw new Error(error.detail); } return response.json() as Promise; } async deleteProfilePicture() { return this.request('/api/auth/profile/picture', { method: 'DELETE', }); } async checkUsername(username: string) { return this.request( `/api/auth/check-username?username=${encodeURIComponent(username)}` ); } async checkMobile(mobile: string) { return this.request( `/api/auth/check-mobile?mobile=${encodeURIComponent(mobile)}` ); } // Admin auth endpoints async listDeletedUsers() { return this.request('/api/auth/users/deleted'); } async restoreUser(userId: number) { return this.request(`/api/auth/users/${userId}/restore`, { method: 'POST', }); } async listUsers(params?: { search?: string; role?: 'staff' | 'superuser'; student_id?: string; university?: string; major?: string; is_active?: 'true' | 'false'; limit?: number; offset?: number; }) { const query = new URLSearchParams(); if (params?.search) query.set('search', params.search); if (params?.role) query.set('role', params.role); if (params?.student_id) query.set('student_id', params.student_id); if (params?.university) query.set('university', params.university); if (params?.major) query.set('major', params.major); if (params?.is_active) query.set('is_active', params.is_active); if (params?.limit != null) query.set('limit', String(params.limit)); if (params?.offset != null) query.set('offset', String(params.offset)); return this.request(`/api/auth/users${query.toString() ? `?${query.toString()}` : ''}`); } async getUserDetail(userId: number) { return this.request(`/api/auth/users/${userId}`); } async listAuthorizationRoles() { return this.request('/api/auth/roles'); } async getUserAuthorization(userId: number) { return this.request(`/api/auth/users/${userId}/authorization`); } async updateUserAuthorization(userId: number, data: Types.UserAuthorizationUpdateSchema) { return this.request(`/api/auth/users/${userId}/authorization`, { method: 'PUT', body: JSON.stringify(data), }); } async getAdminDashboard(params?: { date_from?: string; date_to?: string; event_id?: number; granularity?: 'auto' | 'day' | 'week' | 'month'; }) { const query = new URLSearchParams(); if (params?.date_from) query.set('date_from', params.date_from); if (params?.date_to) query.set('date_to', params.date_to); if (params?.event_id != null) query.set('event_id', String(params.event_id)); if (params?.granularity) query.set('granularity', params.granularity); return this.request( `/api/analytics/admin/dashboard${query.toString() ? `?${query.toString()}` : ''}`, ); } async getAdminUserAnalytics(params?: { date_from?: string; date_to?: string }) { const query = new URLSearchParams(); if (params?.date_from) query.set('date_from', params.date_from); if (params?.date_to) query.set('date_to', params.date_to); return this.request( `/api/analytics/admin/users${query.toString() ? `?${query.toString()}` : ''}`, ); } async getAdminEventAnalytics(params?: { date_from?: string; date_to?: string; event_id?: number }) { const query = new URLSearchParams(); if (params?.date_from) query.set('date_from', params.date_from); if (params?.date_to) query.set('date_to', params.date_to); if (params?.event_id != null) query.set('event_id', String(params.event_id)); return this.request( `/api/analytics/admin/events${query.toString() ? `?${query.toString()}` : ''}`, ); } async getAdminBlogAnalytics(params?: { date_from?: string; date_to?: string }) { const query = new URLSearchParams(); if (params?.date_from) query.set('date_from', params.date_from); if (params?.date_to) query.set('date_to', params.date_to); return this.request( `/api/analytics/admin/blog${query.toString() ? `?${query.toString()}` : ''}`, ); } async getAdminDashboardEventOptions(params?: { search?: string; limit?: number; offset?: number }) { const query = new URLSearchParams(); if (params?.search) query.set('search', params.search); if (params?.limit != null) query.set('limit', String(params.limit)); if (params?.offset != null) query.set('offset', String(params.offset)); return this.request( `/api/analytics/admin/events/options${query.toString() ? `?${query.toString()}` : ''}`, ); } // ============= Blog Endpoints ============= async getPosts(params?: { page?: number; limit?: number; category?: string; tag?: string | string[]; search?: string; featured?: boolean; author?: string | string[]; }) { const queryParams = new URLSearchParams(); if (params?.page) queryParams.append('page', params.page.toString()); if (params?.limit) queryParams.append('limit', params.limit.toString()); if (params?.category) queryParams.append('category', params.category); if (Array.isArray(params?.tag)) { params.tag.forEach((tag) => queryParams.append('tag', tag)); } else if (params?.tag) { queryParams.append('tag', params.tag); } if (params?.search) queryParams.append('search', params.search); if (params?.featured !== undefined) queryParams.append('featured', params.featured.toString()); if (Array.isArray(params?.author)) { params.author.forEach((author) => queryParams.append('author', author)); } else if (params?.author) { queryParams.append('author', params.author); } const query = queryParams.toString(); return this.request(`/api/blog/posts${query ? `?${query}` : ''}`); } async getBlogFilters() { return this.request('/api/blog/filters'); } async getPost(slug: string) { return this.request(`/api/blog/posts/${encodeURIComponent(slug)}`); } async createPost(data: Types.PostCreateSchema) { return this.request('/api/blog/admin/posts', { method: 'POST', body: JSON.stringify(data), }); } async updatePost(postId: number, data: Types.PostCreateSchema) { return this.request(`/api/blog/admin/posts/${postId}`, { method: 'PUT', body: JSON.stringify(data), }); } async listAdminBlogPosts(params?: { status?: string; search?: string; mine?: boolean; limit?: number; offset?: number; }) { const query = new URLSearchParams(); if (params?.status) query.set('status', params.status); if (params?.search) query.set('search', params.search); if (params?.mine != null) query.set('mine', String(params.mine)); if (params?.limit != null) query.set('limit', String(params.limit)); if (params?.offset != null) query.set('offset', String(params.offset)); return this.request(`/api/blog/admin/posts${query.toString() ? `?${query.toString()}` : ''}`); } async listBlogWriters() { return this.request>('/api/blog/admin/writers'); } async getAdminBlogPost(postId: number) { return this.request(`/api/blog/admin/posts/${postId}`); } async submitBlogPost(postId: number) { return this.request(`/api/blog/admin/posts/${postId}/submit`, { method: 'POST', }); } async reviewBlogPost(postId: number, data: Types.PostReviewSchema) { return this.request(`/api/blog/admin/posts/${postId}/review`, { method: 'POST', body: JSON.stringify(data), }); } async listBlogPostAssets(postId: number) { return this.request(`/api/blog/admin/posts/${postId}/assets`); } async uploadBlogPostFeaturedImage(postId: number, file: File) { const formData = new FormData(); formData.append('file', file); const token = this.getStorageValue('access_token'); const response = await fetch(`${this.baseUrl}/api/blog/admin/posts/${postId}/featured-image`, { method: 'POST', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: formData, }); if (!response.ok) { const error = (await response.json().catch(() => ({}))) as ApiErrorBody; throw new Error(error.error || error.detail || 'Featured image upload failed'); } return response.json() as Promise; } async deleteBlogPostFeaturedImage(postId: number) { return this.request(`/api/blog/admin/posts/${postId}/featured-image`, { method: 'DELETE', }); } async uploadBlogPostAsset( postId: number, file: File, data: { title?: string; alt_text?: string; caption?: string } = {}, ) { const formData = new FormData(); formData.append('file', file); formData.append('title', data.title ?? ''); formData.append('alt_text', data.alt_text ?? ''); formData.append('caption', data.caption ?? ''); const token = this.getStorageValue('access_token'); const response = await fetch(`${this.baseUrl}/api/blog/admin/posts/${postId}/assets`, { method: 'POST', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: formData, }); if (!response.ok) { const error = (await response.json().catch(() => ({}))) as ApiErrorBody; throw new Error(error.error || error.detail || 'Asset upload failed'); } return response.json() as Promise; } uploadBlogPostAssetWithProgress( postId: number, file: File, data: { title?: string; alt_text?: string; caption?: string } = {}, onProgress?: (progress: number) => void, ) { const formData = new FormData(); formData.append('file', file); formData.append('title', data.title ?? ''); formData.append('alt_text', data.alt_text ?? ''); formData.append('caption', data.caption ?? ''); const token = this.getStorageValue('access_token'); return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); request.open('POST', `${this.baseUrl}/api/blog/admin/posts/${postId}/assets`); if (token) { request.setRequestHeader('Authorization', `Bearer ${token}`); } request.upload.onprogress = (event) => { if (!event.lengthComputable) return; onProgress?.(Math.round((event.loaded / event.total) * 100)); }; request.onload = () => { let body: (Types.PostAssetSchema & ApiErrorBody) | null = null; try { body = request.responseText ? JSON.parse(request.responseText) as Types.PostAssetSchema & ApiErrorBody : null; } catch { body = null; } if (request.status >= 200 && request.status < 300 && body) { onProgress?.(100); resolve(body); return; } reject(new Error(body?.error || body?.detail || 'Asset upload failed')); }; request.onerror = () => reject(new Error('Asset upload failed')); request.send(formData); }); } async deleteBlogPostAsset(postId: number, assetId: number) { return this.request(`/api/blog/admin/posts/${postId}/assets/${assetId}`, { method: 'DELETE', }); } async deletePost(slug: string) { return this.request(`/api/blog/posts/${encodeURIComponent(slug)}`, { method: 'DELETE', }); } async listDeletedPosts() { return this.request('/api/blog/deleted/posts'); } async restorePost(postId: number) { return this.request(`/api/blog/deleted/posts/${postId}/restore`, { method: 'POST', }); } // Comments async getComments(slug: string) { return this.request(`/api/blog/posts/${encodeURIComponent(slug)}/comments`); } async createComment(slug: string, data: Types.CommentCreateSchema) { return this.request(`/api/blog/posts/${encodeURIComponent(slug)}/comments`, { method: 'POST', body: JSON.stringify(data), }); } async updateComment(commentId: number, data: Types.CommentUpdateSchema) { return this.request(`/api/blog/comments/${commentId}`, { method: 'PUT', body: JSON.stringify(data), }); } async hideComment(commentId: number, note?: string) { return this.request(`/api/blog/comments/${commentId}/hide`, { method: 'POST', body: JSON.stringify({ note: note ?? '' }), }); } async unhideComment(commentId: number) { return this.request(`/api/blog/comments/${commentId}/unhide`, { method: 'POST', }); } async deleteComment(commentId: number, note?: string) { return this.request(`/api/blog/comments/${commentId}/delete`, { method: 'POST', body: JSON.stringify({ note: note ?? '' }), }); } async listDeletedComments() { return this.request('/api/blog/deleted/comments'); } async restoreComment(commentId: number) { return this.request(`/api/blog/deleted/comments/${commentId}/restore`, { method: 'POST', }); } // Likes async toggleLike(slug: string) { return this.request(`/api/blog/posts/${encodeURIComponent(slug)}/like`, { method: 'POST', }); } async toggleSave(slug: string) { return this.request(`/api/blog/posts/${encodeURIComponent(slug)}/save`, { method: 'POST', }); } async getBlogInteraction(slug: string) { return this.request(`/api/blog/posts/${encodeURIComponent(slug)}/interaction`); } async getLikesCount(slug: string) { return this.request(`/api/blog/posts/${encodeURIComponent(slug)}/likes`); } async getMyBlogActivity() { return this.request('/api/blog/me/activity'); } async getMyBlogPostsPage(section: "liked" | "saved", page: number) { return this.request>( `/api/blog/me/activity/${section}?page=${page}`, ); } async getMyBlogCommentsPage(page: number) { return this.request>( `/api/blog/me/activity/comments?page=${page}`, ); } // Categories async getCategories() { return this.request('/api/blog/categories'); } async listAdminCategories() { return this.request('/api/blog/admin/categories'); } async createCategory(data: Types.CategoryWriteSchema) { return this.request('/api/blog/admin/categories', { method: 'POST', body: JSON.stringify(data), }); } async updateCategory(categoryId: number, data: Types.CategoryWriteSchema) { return this.request(`/api/blog/admin/categories/${categoryId}`, { method: 'PUT', body: JSON.stringify(data), }); } async deleteCategory(categoryId: number) { return this.request(`/api/blog/admin/categories/${categoryId}`, { method: 'DELETE', }); } async getCategory(slug: string) { return this.request(`/api/blog/categories/${slug}`); } async listDeletedCategories() { return this.request('/api/blog/deleted/categories'); } async restoreCategory(categoryId: number) { return this.request(`/api/blog/deleted/categories/${categoryId}/restore`, { method: 'POST', }); } // Tags async getTags() { return this.request('/api/blog/tags'); } async listAdminTags() { return this.request('/api/blog/admin/tags'); } async createTag(data: Types.TagWriteSchema) { return this.request('/api/blog/admin/tags', { method: 'POST', body: JSON.stringify(data), }); } async updateTag(tagId: number, data: Types.TagWriteSchema) { return this.request(`/api/blog/admin/tags/${tagId}`, { method: 'PUT', body: JSON.stringify(data), }); } async deleteTag(tagId: number) { return this.request(`/api/blog/admin/tags/${tagId}`, { method: 'DELETE', }); } async getTag(slug: string) { return this.request(`/api/blog/tags/${slug}`); } async listDeletedTags() { return this.request('/api/blog/deleted/tags'); } async restoreTag(tagId: number) { return this.request(`/api/blog/deleted/tags/${tagId}/restore`, { method: 'POST', }); } // ============= Events Endpoints ============= async getEvents(params: { status?: 'draft' | 'published' | 'cancelled' | 'completed'; statuses?: Array<'draft' | 'published' | 'cancelled' | 'completed'>; // جدید: چندتا وضعیت event_type?: 'online' | 'on_site' | 'hybrid'; search?: string; limit?: number; offset?: number; } = {}) { const q = new URLSearchParams(); if (params.statuses?.length) { params.statuses.forEach(s => q.append('status', s)); } else if (params.status) { q.set('status', params.status); } if (params.event_type) q.set('event_type', params.event_type); if (params.search) q.set('search', params.search); if (params.limit != null) q.set('limit', String(params.limit)); if (params.offset != null) q.set('offset', String(params.offset)); const url = `/api/events/${q.toString() ? `?${q.toString()}` : ''}`; return this.request(url, { method: 'GET' }); } async browseEvents(params: { state?: 'all' | 'completed' | 'upcoming' | 'open'; event_type?: 'online' | 'on_site' | 'hybrid'; search?: string; date_from?: string; date_to?: string; price_min?: number; price_max?: number; free_only?: boolean; page?: number; } = {}) { const query = new URLSearchParams(); if (params.state) query.set('state', params.state); if (params.event_type) query.set('event_type', params.event_type); if (params.search?.trim()) query.set('search', params.search.trim()); if (params.date_from) query.set('date_from', params.date_from); if (params.date_to) query.set('date_to', params.date_to); if (params.price_min != null) query.set('price_min', String(params.price_min)); if (params.price_max != null) query.set('price_max', String(params.price_max)); if (params.free_only) query.set('free_only', 'true'); if (params.page != null) query.set('page', String(params.page)); return this.request( `/api/events/browse/${query.size ? `?${query.toString()}` : ''}`, { method: 'GET' }, ); } async getEventBySlug(slug: string) { return this.request(`/api/events/slug/${encodeURIComponent(slug)}`, { method: 'GET' }); } async getEventById(eventId: number) { return this.request(`/api/events/${eventId}`, { method: 'GET' }); } async getEventAdminDetail(eventId: number) { return this.request(`/api/events/${eventId}/admin-detail`); } async listEventRegistrationsAdmin( eventId: number, params?: { statuses?: string[]; university?: string; major?: string; search?: string; limit?: number; offset?: number; } ) { const query = new URLSearchParams(); if (params?.statuses?.length) { params.statuses.forEach((status) => query.append('status', status)); } if (params?.university) query.set('university', params.university); if (params?.major) query.set('major', params.major); if (params?.search) query.set('search', params.search); if (params?.limit != null) query.set('limit', String(params.limit)); if (params?.offset != null) query.set('offset', String(params.offset)); return this.request>( `/api/events/${eventId}/admin-registrations${query.toString() ? `?${query.toString()}` : ''}` ); } async updateEvent(eventId: number, data: Types.EventUpdateSchema) { return this.request(`/api/events/${eventId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } async createEvent(data: Types.EventCreateSchema) { return this.request('/api/events/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } async uploadEventFeaturedImage(eventId: number, file: File) { const formData = new FormData(); formData.append('file', file); const token = this.getStorageValue('access_token'); const response = await fetch(`${this.baseUrl}/api/events/${eventId}/featured-image`, { method: 'POST', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: formData, }); if (!response.ok) { const body = (await response.json().catch(() => ({}))) as ApiErrorBody; throw new Error(body.error || body.detail || 'Event image upload failed'); } return response.json() as Promise; } async deleteEventFeaturedImage(eventId: number) { return this.request(`/api/events/${eventId}/featured-image`, { method: 'DELETE', }); } async listEventGallery(eventId: number) { return this.request(`/api/events/${eventId}/gallery`); } async uploadEventGalleryImage(eventId: number, file: File, data: { title?: string; alt_text?: string } = {}) { const formData = new FormData(); formData.append('file', file); if (data.title) formData.append('title', data.title); if (data.alt_text) formData.append('alt_text', data.alt_text); const token = this.getStorageValue('access_token'); const response = await fetch(`${this.baseUrl}/api/events/${eventId}/gallery`, { method: 'POST', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: formData, }); if (!response.ok) { const body = (await response.json().catch(() => ({}))) as ApiErrorBody; throw new Error(body.error || body.detail || 'Event gallery upload failed'); } return response.json() as Promise; } async deleteEventGalleryImage(eventId: number, imageId: number) { return this.request(`/api/events/${eventId}/gallery/${imageId}`, { method: 'DELETE', }); } async deleteEvent(eventId: number) { return this.request(`/api/events/${eventId}`, { method: 'DELETE', }); } async registerForEvent(eventId: number, discountCode?: string | null) { const payload = (discountCode ?? '').trim(); const init: RequestInit = { method: 'POST' }; if (payload) { init.headers = { 'Content-Type': 'application/json' }; init.body = JSON.stringify({ discount_code: payload }); } return this.request(`/api/events/${eventId}/register`, init); } async ChangeRegistrationStatus(registrationId: number, status: string) { return this.request( `/api/events/registrations/${registrationId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: status }), } ); } async listEventRegistrations(eventId: number, limit = 20, offset = 0) { const url = `/api/events/${eventId}/registrations?limit=${limit}&offset=${offset}`; return this.request(url, { method: 'GET' }); } async cancelEventRegistration(eventId: number) { return this.request(`/api/events/${eventId}/register`, { method: 'DELETE', }); } async cancelRegistrationCheckout(eventId: number) { return this.request(`/api/events/${eventId}/checkout/cancel`, { method: "POST", }); } async verifyMyRegistration(ticket_id: string) { return this.request<{ event_image: string; event_title: string; event_type: string; ticket_id: string; status: string; registered_at: string; success_markdown: string; }>(`/api/events/registerations/verify/${ticket_id}`, {method: 'GET'}); } async getMyRegistrations() { return this.request( `/api/events/my-registrations`, { method: 'GET' } ); } async getMyRegistrationsPage( status: Types.MyEventRegistrationSchema["status"] | "all", sortBy: "start_time" | "title", sortOrder: "asc" | "desc", page: number, ) { const params = new URLSearchParams({ status, sort_by: sortBy, sort_order: sortOrder, page: String(page), }); return this.request>( `/api/events/my-registrations/paginated?${params.toString()}`, { method: "GET" }, ); } async getMyPayments() { return this.request("/api/payments/me", { method: "GET" }); } async getMyPaymentsPage(page: number) { return this.request>( `/api/payments/me/paginated?page=${page}`, { method: "GET" }, ); } async getRegistrationStatus(eventId: number) { return this.request( `/api/events/${eventId}/is-registered`, { method: 'GET' } ); } async getRegistrationHubStatus( eventSlug: string, identifiers: { registration_id?: string | null; payment_id?: number | null } = {}, ) { const query = new URLSearchParams(); if (identifiers.registration_id) query.set("registration_id", identifiers.registration_id); if (identifiers.payment_id) query.set("payment_id", String(identifiers.payment_id)); const suffix = query.size ? `?${query.toString()}` : ""; return this.request( `/api/events/${encodeURIComponent(eventSlug)}/registration/status${suffix}`, { method: "GET" }, ); } // ============= Payment Endpoints ============= async createPayment(input: { event_id: number; description: string; discount_code?: string | null; mobile?: string | null; email?: string | null; idempotency_key: string; }) { return this.request( '/api/payments/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), } ); } async getPaymentStatus(paymentId: number) { return this.request(`/api/payments/status/${paymentId}`, { method: 'GET' }); } async checkDiscountCode(event_id: number, code: string) { return this.request<{ discount_amount: number; final_price: number; }>( `/api/payments/coupon/check`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({code: code, event_id: event_id}), } ); } async listDiscountCodes(params?: { search?: string; is_active?: boolean; type?: 'percent' | 'fixed'; limit?: number; offset?: number; }) { const query = new URLSearchParams(); if (params?.search) query.set('search', params.search); if (params?.is_active != null) query.set('is_active', String(params.is_active)); if (params?.type) query.set('type', params.type); if (params?.limit != null) query.set('limit', String(params.limit)); if (params?.offset != null) query.set('offset', String(params.offset)); return this.request( `/api/payments/admin/discount-codes${query.toString() ? `?${query.toString()}` : ''}`, ); } async createDiscountCode(data: Types.DiscountCodeWriteSchema) { return this.request('/api/payments/admin/discount-codes', { method: 'POST', body: JSON.stringify(data), }); } async updateDiscountCode(codeId: number, data: Types.DiscountCodeWriteSchema) { return this.request(`/api/payments/admin/discount-codes/${codeId}`, { method: 'PUT', body: JSON.stringify(data), }); } async deleteDiscountCode(codeId: number) { return this.request(`/api/payments/admin/discount-codes/${codeId}`, { method: 'DELETE', }); } // ============= Gallery Endpoints ============= async getGalleryImages(params?: { page?: number; limit?: number; tag?: string; }) { const queryParams = new URLSearchParams(); if (params?.page) queryParams.append('page', params.page.toString()); if (params?.limit) queryParams.append('limit', params.limit.toString()); if (params?.tag) queryParams.append('tag', params.tag); const query = queryParams.toString(); return this.request(`/api/gallery/images${query ? `?${query}` : ''}`); } async uploadGalleryImage(file: File, data: Types.GalleryImageCreateSchema) { const formData = new FormData(); formData.append('file', file); formData.append('title', data.title); if (data.description) formData.append('description', data.description); if (data.tag_ids) formData.append('tag_ids', JSON.stringify(data.tag_ids)); const token = this.getStorageValue('access_token'); const response = await fetch(`${this.baseUrl}/api/gallery/images`, { method: 'POST', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: formData, }); if (!response.ok) { const error: Types.ErrorSchema = await response.json().catch(() => ({ detail: 'خطای آپلود تصویر', })); throw new Error(error.detail); } return response.json() as Promise; } async deleteGalleryImage(imageId: number) { return this.request(`/api/gallery/images/${imageId}`, { method: 'DELETE', }); } async getMajors(params?: { search?: string; limit?: number; offset?: number }): Promise { const data = await this.getMajorsPaged({ limit: 100, offset: 0, ...params }); return data.results.map((item) => ({ code: item.code, label: item.label, id: item.id, name: item.name, user_count: item.user_count })); } async getUniversities(params?: { search?: string; limit?: number; offset?: number }): Promise { const data = await this.getUniversitiesPaged({ limit: 100, offset: 0, ...params }); return data.results.map((item) => ({ code: item.code, label: item.label, id: item.id, name: item.name, user_count: item.user_count })); } async getMajorsPaged(params?: { search?: string; limit?: number; offset?: number }) { const query = new URLSearchParams(); if (params?.search) query.set('search', params.search); if (params?.limit != null) query.set('limit', String(params.limit)); if (params?.offset != null) query.set('offset', String(params.offset)); return this.request(`/api/meta/majors${query.toString() ? `?${query.toString()}` : ''}`); } async getUniversitiesPaged(params?: { search?: string; limit?: number; offset?: number }) { const query = new URLSearchParams(); if (params?.search) query.set('search', params.search); if (params?.limit != null) query.set('limit', String(params.limit)); if (params?.offset != null) query.set('offset', String(params.offset)); return this.request(`/api/meta/universities${query.toString() ? `?${query.toString()}` : ''}`); } async listAdminMajors(params?: { search?: string; limit?: number; offset?: number }) { const query = new URLSearchParams(); if (params?.search) query.set('search', params.search); if (params?.limit != null) query.set('limit', String(params.limit)); if (params?.offset != null) query.set('offset', String(params.offset)); return this.request(`/api/meta/admin/majors${query.toString() ? `?${query.toString()}` : ''}`); } async createMajor(data: Types.MetaOptionWriteSchema) { return this.request('/api/meta/admin/majors', { method: 'POST', body: JSON.stringify(data), }); } async updateMajor(id: number, data: Types.MetaOptionWriteSchema) { return this.request(`/api/meta/admin/majors/${id}`, { method: 'PUT', body: JSON.stringify(data), }); } async deleteMajor(id: number) { return this.request(`/api/meta/admin/majors/${id}`, { method: 'DELETE', }); } async listAdminUniversities(params?: { search?: string; limit?: number; offset?: number }) { const query = new URLSearchParams(); if (params?.search) query.set('search', params.search); if (params?.limit != null) query.set('limit', String(params.limit)); if (params?.offset != null) query.set('offset', String(params.offset)); return this.request(`/api/meta/admin/universities${query.toString() ? `?${query.toString()}` : ''}`); } async createUniversity(data: Types.MetaOptionWriteSchema) { return this.request('/api/meta/admin/universities', { method: 'POST', body: JSON.stringify(data), }); } async updateUniversity(id: number, data: Types.MetaOptionWriteSchema) { return this.request(`/api/meta/admin/universities/${id}`, { method: 'PUT', body: JSON.stringify(data), }); } async deleteUniversity(id: number) { return this.request(`/api/meta/admin/universities/${id}`, { method: 'DELETE', }); } async getNotifications(params?: { limit?: number; offset?: number; type?: string; status?: 'all' | 'unread' | 'read' }) { const query = new URLSearchParams(); if (params?.limit != null) query.set('limit', String(params.limit)); if (params?.offset != null) query.set('offset', String(params.offset)); if (params?.type) query.set('type', params.type); if (params?.status) query.set('status', params.status); return this.request( `/api/notifications/${query.toString() ? `?${query.toString()}` : ''}` ); } async getNotification(id: string) { return this.request(`/api/notifications/${encodeURIComponent(id)}`); } async markNotificationSeen(id: string) { return this.request('/api/notifications/mark-seen', { method: 'POST', body: JSON.stringify({ id }), }); } async deleteNotification(id: string) { return this.request(`/api/notifications/${encodeURIComponent(id)}`, { method: 'DELETE', }); } async markAllNotificationsRead(type?: string) { const query = type ? `?type=${encodeURIComponent(type)}` : ''; return this.request<{ marked_read: number }>(`/api/notifications/mark-all-read${query}`, { method: 'POST', }); } async issueNotificationStreamToken() { return this.request('/api/notifications/stream-token', { method: 'POST', }); } buildNotificationStreamUrl(token: string) { const cleanBaseUrl = this.baseUrl.replace(/\/+$/, ''); return `${cleanBaseUrl}/api/notifications/stream/?token=${encodeURIComponent(token)}`; } } export const api = new ApiClient(API_BASE_URL);