import type * as Types from './types'; import { apiBaseUrl } from '@/lib/site'; 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 resetPassword(data: Types.PasswordResetSchema) { return this.request('/api/auth/reset-password', { 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() { if (typeof window !== 'undefined') { 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 verifyEmail(token: string): Promise { const url = `${this.baseUrl}/api/auth/verify-email/${encodeURIComponent(token)}`; const response = await fetch(url, { method: 'GET' }); if (response.ok) { return response.json() as Promise; } const data = (await response.json().catch(() => ({}))) as ApiErrorBody; const errMsg: string = (data && (data.error || data.detail)) || 'خطای ناشناخته رخ داد'; throw new Error(errMsg); } async resendVerification(email: string) { return this.request( `/api/auth/resend-verification?email=${encodeURIComponent(email)}`, { method: 'POST' } ); } async getProfile() { return this.request('/api/auth/profile' ); } 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 requestPasswordReset(email: string) { return this.request('/api/auth/request-password-reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); } async resetPasswordConfirm(token: string, new_password: string) { return this.request('/api/auth/reset-password-confirm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, new_password }), }); } async getLegacyVerifyEmailMessage(token: string) { return this.request(`/api/auth/verify-email/${encodeURIComponent(token)}`); } async getLegacyResetTokenMessage(token: string) { return this.request('/api/auth/reset-password-confirm', { method: 'POST', body: JSON.stringify({ token }), }); } 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), }); } // ============= 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'); } // 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 getEventBySlug(slug: string) { return this.request(`/api/events/slug/${encodeURIComponent(slug)}`, { 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 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 getRegistrationStatus(eventId: number) { return this.request( `/api/events/${eventId}/is-registered`, { method: 'GET' } ); } // ============= Payment Endpoints ============= async createPayment(input: { event_id: number; description: string; discount_code?: string | null; mobile?: string | null; email?: string | null; }) { return this.request( '/api/payments/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), } ); } async getPaymentByRef(refId: string) { return this.request<{ ref_id: string; authority: string; base_amount: number; discount_amount: number; amount: number; status: 'INIT' | 'PENDING' | 'PAID' | 'FAILED' | 'CANCELED'; verified_at?: string | null; event: { id: number; title: string; slug: string; image_url?: string | null; success_markdown?: string | null; }; }>(`/api/payments/by-ref/${encodeURIComponent(refId)}`, { 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 subscribeNewsletter(email: string) { return this.request<{ message: string, success: boolean }>( `/api/communications/newsletter/subscribe/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email }), } ); } async getNotifications(params?: { limit?: number; offset?: number; type?: string }) { 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); return this.request( `/api/notifications/${query.toString() ? `?${query.toString()}` : ''}` ); } 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);