Files
guilan-ace-frontend/src/lib/api.ts

1262 lines
40 KiB
TypeScript

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<string> {
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<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
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<T>;
}
// ============= Auth Endpoints =============
async register(data: Types.UserRegistrationSchema) {
return this.request<Types.MessageSchema>('/api/auth/register', {
method: 'POST',
body: JSON.stringify(data),
});
}
async login(data: Types.UserLoginSchema) {
return this.request<Types.TokenSchema>('/api/auth/login', {
method: 'POST',
body: JSON.stringify(data),
});
}
async loginWithOtp(data: Types.UserOtpLoginSchema) {
return this.request<Types.TokenSchema>('/api/auth/login/otp', {
method: 'POST',
body: JSON.stringify(data),
});
}
async sendOtp(data: Types.OtpSendSchema) {
return this.request<Types.OtpSendResponseSchema>('/api/auth/otp/send', {
method: 'POST',
body: JSON.stringify(data),
});
}
async verifyRegisterOtp(data: Types.RegisterOtpVerifySchema) {
return this.request<Types.MessageSchema>('/api/auth/otp/verify-register', {
method: 'POST',
body: JSON.stringify(data),
});
}
async refreshToken(data: Types.TokenRefreshIn) {
return this.request<Types.TokenSchema>('/api/auth/refresh', {
method: 'POST',
body: JSON.stringify(data),
});
}
async resetPassword(data: Types.PasswordResetSchema) {
return this.request<Types.MessageSchema>('/api/auth/reset-password', {
method: 'POST',
body: JSON.stringify(data),
});
}
async sendMobileVerificationOtp(data: Types.MobileOtpSendSchema) {
return this.request<Types.OtpSendResponseSchema>('/api/auth/mobile/send-otp', {
method: 'POST',
body: JSON.stringify(data),
});
}
async verifyMobile(data: Types.MobileOtpVerifySchema) {
return this.request<Types.UserProfileSchema>('/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<Types.GoogleFlowResponseSchema>(
`/api/auth/oauth/google/flow?flow=${encodeURIComponent(flow)}`
);
}
async completeGoogleSignup(data: Types.GoogleCompleteSchema) {
return this.request<Types.GoogleFlowResponseSchema>('/api/auth/oauth/google/complete', {
method: 'POST',
body: JSON.stringify(data),
});
}
async resendGoogleClaimOtp(flow: string) {
return this.request<Types.MessageSchema>('/api/auth/oauth/google/claim/send-otp', {
method: 'POST',
body: JSON.stringify({ flow }),
});
}
async verifyGoogleClaim(flow: string, code: string) {
return this.request<Types.GoogleFlowResponseSchema>('/api/auth/oauth/google/claim/verify', {
method: 'POST',
body: JSON.stringify({ flow, code }),
});
}
async verifyEmail(token: string): Promise<Types.MessageSchema> {
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<Types.MessageSchema>;
}
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<Types.MessageSchema>(
`/api/auth/resend-verification?email=${encodeURIComponent(email)}`,
{ method: 'POST' }
);
}
async getProfile() {
return this.request<Types.UserProfileSchema>('/api/auth/profile'
);
}
async updateProfile(data: Types.UserUpdateSchema) {
return this.request<Types.UserProfileSchema>('/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<Types.MessageSchema>;
}
async deleteProfilePicture() {
return this.request<Types.MessageSchema>('/api/auth/profile/picture', {
method: 'DELETE',
});
}
async requestPasswordReset(email: string) {
return this.request<Types.MessageSchema>('/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<Types.MessageSchema>('/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<Types.MessageSchema>(`/api/auth/verify-email/${encodeURIComponent(token)}`);
}
async getLegacyResetTokenMessage(token: string) {
return this.request<Types.MessageSchema>('/api/auth/reset-password-confirm', {
method: 'POST',
body: JSON.stringify({ token }),
});
}
async checkUsername(username: string) {
return this.request<Types.UsernameCheckSchema>(
`/api/auth/check-username?username=${encodeURIComponent(username)}`
);
}
async checkMobile(mobile: string) {
return this.request<Types.MobileLookupSchema>(
`/api/auth/check-mobile?mobile=${encodeURIComponent(mobile)}`
);
}
// Admin auth endpoints
async listDeletedUsers() {
return this.request<Types.UserProfileSchema[]>('/api/auth/users/deleted');
}
async restoreUser(userId: number) {
return this.request<Types.MessageSchema>(`/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<Types.UserListSchema[]>(`/api/auth/users${query.toString() ? `?${query.toString()}` : ''}`);
}
async getUserDetail(userId: number) {
return this.request<Types.UserProfileSchema>(`/api/auth/users/${userId}`);
}
async listAuthorizationRoles() {
return this.request<Types.AuthorizationRoleSchema[]>('/api/auth/roles');
}
async getUserAuthorization(userId: number) {
return this.request<Types.UserAuthorizationSchema>(`/api/auth/users/${userId}/authorization`);
}
async updateUserAuthorization(userId: number, data: Types.UserAuthorizationUpdateSchema) {
return this.request<Types.UserAuthorizationSchema>(`/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<Types.PostListSchema[]>(`/api/blog/posts${query ? `?${query}` : ''}`);
}
async getBlogFilters() {
return this.request<Types.BlogFiltersSchema>('/api/blog/filters');
}
async getPost(slug: string) {
return this.request<Types.PostDetailSchema>(`/api/blog/posts/${encodeURIComponent(slug)}`);
}
async createPost(data: Types.PostCreateSchema) {
return this.request<Types.PostDetailSchema>('/api/blog/admin/posts', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updatePost(postId: number, data: Types.PostCreateSchema) {
return this.request<Types.PostDetailSchema>(`/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<Types.PostListSchema[]>(`/api/blog/admin/posts${query.toString() ? `?${query.toString()}` : ''}`);
}
async listBlogWriters() {
return this.request<NonNullable<Types.PostListSchema['writers']>>('/api/blog/admin/writers');
}
async getAdminBlogPost(postId: number) {
return this.request<Types.PostDetailSchema>(`/api/blog/admin/posts/${postId}`);
}
async submitBlogPost(postId: number) {
return this.request<Types.PostDetailSchema>(`/api/blog/admin/posts/${postId}/submit`, {
method: 'POST',
});
}
async reviewBlogPost(postId: number, data: Types.PostReviewSchema) {
return this.request<Types.PostDetailSchema>(`/api/blog/admin/posts/${postId}/review`, {
method: 'POST',
body: JSON.stringify(data),
});
}
async listBlogPostAssets(postId: number) {
return this.request<Types.PostAssetSchema[]>(`/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<Types.PostDetailSchema>;
}
async deleteBlogPostFeaturedImage(postId: number) {
return this.request<Types.PostDetailSchema>(`/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<Types.PostAssetSchema>;
}
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<Types.PostAssetSchema>((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<Types.MessageSchema>(`/api/blog/admin/posts/${postId}/assets/${assetId}`, {
method: 'DELETE',
});
}
async deletePost(slug: string) {
return this.request<Types.MessageSchema>(`/api/blog/posts/${encodeURIComponent(slug)}`, {
method: 'DELETE',
});
}
async listDeletedPosts() {
return this.request<Types.PostListSchema[]>('/api/blog/deleted/posts');
}
async restorePost(postId: number) {
return this.request<Types.MessageSchema>(`/api/blog/deleted/posts/${postId}/restore`, {
method: 'POST',
});
}
// Comments
async getComments(slug: string) {
return this.request<Types.CommentSchema[]>(`/api/blog/posts/${encodeURIComponent(slug)}/comments`);
}
async createComment(slug: string, data: Types.CommentCreateSchema) {
return this.request<Types.CommentSchema>(`/api/blog/posts/${encodeURIComponent(slug)}/comments`, {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateComment(commentId: number, data: Types.CommentUpdateSchema) {
return this.request<Types.CommentSchema>(`/api/blog/comments/${commentId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async hideComment(commentId: number, note?: string) {
return this.request<Types.MessageSchema>(`/api/blog/comments/${commentId}/hide`, {
method: 'POST',
body: JSON.stringify({ note: note ?? '' }),
});
}
async unhideComment(commentId: number) {
return this.request<Types.MessageSchema>(`/api/blog/comments/${commentId}/unhide`, {
method: 'POST',
});
}
async deleteComment(commentId: number, note?: string) {
return this.request<Types.MessageSchema>(`/api/blog/comments/${commentId}/delete`, {
method: 'POST',
body: JSON.stringify({ note: note ?? '' }),
});
}
async listDeletedComments() {
return this.request<Types.CommentSchema[]>('/api/blog/deleted/comments');
}
async restoreComment(commentId: number) {
return this.request<Types.MessageSchema>(`/api/blog/deleted/comments/${commentId}/restore`, {
method: 'POST',
});
}
// Likes
async toggleLike(slug: string) {
return this.request<Types.BlogInteractionSchema>(`/api/blog/posts/${encodeURIComponent(slug)}/like`, {
method: 'POST',
});
}
async toggleSave(slug: string) {
return this.request<Types.BlogInteractionSchema>(`/api/blog/posts/${encodeURIComponent(slug)}/save`, {
method: 'POST',
});
}
async getBlogInteraction(slug: string) {
return this.request<Types.BlogInteractionSchema>(`/api/blog/posts/${encodeURIComponent(slug)}/interaction`);
}
async getLikesCount(slug: string) {
return this.request<Types.MessageSchema>(`/api/blog/posts/${encodeURIComponent(slug)}/likes`);
}
async getMyBlogActivity() {
return this.request<Types.BlogProfileActivitySchema>('/api/blog/me/activity');
}
// Categories
async getCategories() {
return this.request<Types.CategorySchema[]>('/api/blog/categories');
}
async listAdminCategories() {
return this.request<Types.AdminCategorySchema[]>('/api/blog/admin/categories');
}
async createCategory(data: Types.CategoryWriteSchema) {
return this.request<Types.AdminCategorySchema>('/api/blog/admin/categories', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateCategory(categoryId: number, data: Types.CategoryWriteSchema) {
return this.request<Types.AdminCategorySchema>(`/api/blog/admin/categories/${categoryId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteCategory(categoryId: number) {
return this.request<Types.MessageSchema>(`/api/blog/admin/categories/${categoryId}`, {
method: 'DELETE',
});
}
async getCategory(slug: string) {
return this.request<Types.CategorySchema>(`/api/blog/categories/${slug}`);
}
async listDeletedCategories() {
return this.request<Types.CategorySchema[]>('/api/blog/deleted/categories');
}
async restoreCategory(categoryId: number) {
return this.request<Types.MessageSchema>(`/api/blog/deleted/categories/${categoryId}/restore`, {
method: 'POST',
});
}
// Tags
async getTags() {
return this.request<Types.TagSchema[]>('/api/blog/tags');
}
async listAdminTags() {
return this.request<Types.AdminTagSchema[]>('/api/blog/admin/tags');
}
async createTag(data: Types.TagWriteSchema) {
return this.request<Types.AdminTagSchema>('/api/blog/admin/tags', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateTag(tagId: number, data: Types.TagWriteSchema) {
return this.request<Types.AdminTagSchema>(`/api/blog/admin/tags/${tagId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteTag(tagId: number) {
return this.request<Types.MessageSchema>(`/api/blog/admin/tags/${tagId}`, {
method: 'DELETE',
});
}
async getTag(slug: string) {
return this.request<Types.TagSchema>(`/api/blog/tags/${slug}`);
}
async listDeletedTags() {
return this.request<Types.TagSchema[]>('/api/blog/deleted/tags');
}
async restoreTag(tagId: number) {
return this.request<Types.MessageSchema>(`/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<Types.EventListItemSchema[]>(url, { method: 'GET' });
}
async getEventBySlug(slug: string) {
return this.request<Types.EventDetailSchema>(`/api/events/slug/${encodeURIComponent(slug)}`, { method: 'GET' });
}
async getEventAdminDetail(eventId: number) {
return this.request<Types.EventAdminDetailSchema>(`/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<Types.PaginatedResponse<Types.RegistrationAdminSchema>>(
`/api/events/${eventId}/admin-registrations${query.toString() ? `?${query.toString()}` : ''}`
);
}
async updateEvent(eventId: number, data: Types.EventUpdateSchema) {
return this.request<Types.EventDetailSchema>(`/api/events/${eventId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
async createEvent(data: Types.EventCreateSchema) {
return this.request<Types.EventDetailSchema>('/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<Types.EventDetailSchema>;
}
async deleteEventFeaturedImage(eventId: number) {
return this.request<Types.EventDetailSchema>(`/api/events/${eventId}/featured-image`, {
method: 'DELETE',
});
}
async listEventGallery(eventId: number) {
return this.request<Types.EventGalleryItem[]>(`/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<Types.EventGalleryItem>;
}
async deleteEventGalleryImage(eventId: number, imageId: number) {
return this.request<Types.MessageSchema>(`/api/events/${eventId}/gallery/${imageId}`, {
method: 'DELETE',
});
}
async deleteEvent(eventId: number) {
return this.request<Types.MessageSchema>(`/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<Types.EventRegistrationSchema>(`/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<Types.EventRegistrationSchema[]>(url, { method: 'GET' });
}
async cancelEventRegistration(eventId: number) {
return this.request<Types.MessageSchema>(`/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<Types.MyEventRegistrationSchema[]>(
`/api/events/my-registrations`,
{ method: 'GET' }
);
}
async getRegistrationStatus(eventId: number) {
return this.request<Types.RegistrationStatusSchema>(
`/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<Types.CreatePaymentOut>(
'/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<Types.PagedDiscountCodeSchema>(
`/api/payments/admin/discount-codes${query.toString() ? `?${query.toString()}` : ''}`,
);
}
async createDiscountCode(data: Types.DiscountCodeWriteSchema) {
return this.request<Types.DiscountCodeSchema>('/api/payments/admin/discount-codes', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateDiscountCode(codeId: number, data: Types.DiscountCodeWriteSchema) {
return this.request<Types.DiscountCodeSchema>(`/api/payments/admin/discount-codes/${codeId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteDiscountCode(codeId: number) {
return this.request<Types.MessageSchema>(`/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<Types.GalleryImageSchema[]>(`/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<Types.GalleryImageSchema>;
}
async deleteGalleryImage(imageId: number) {
return this.request<Types.MessageSchema>(`/api/gallery/images/${imageId}`, {
method: 'DELETE',
});
}
async getMajors(params?: { search?: string; limit?: number; offset?: number }): Promise<Types.MajorOption[]> {
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<Types.MajorOption[]> {
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<Types.PagedMetaOptionSchema>(`/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<Types.PagedMetaOptionSchema>(`/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<Types.PagedMetaOptionSchema>(`/api/meta/admin/majors${query.toString() ? `?${query.toString()}` : ''}`);
}
async createMajor(data: Types.MetaOptionWriteSchema) {
return this.request<Types.MetaOptionSchema>('/api/meta/admin/majors', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateMajor(id: number, data: Types.MetaOptionWriteSchema) {
return this.request<Types.MetaOptionSchema>(`/api/meta/admin/majors/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteMajor(id: number) {
return this.request<Types.MessageSchema>(`/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<Types.PagedMetaOptionSchema>(`/api/meta/admin/universities${query.toString() ? `?${query.toString()}` : ''}`);
}
async createUniversity(data: Types.MetaOptionWriteSchema) {
return this.request<Types.MetaOptionSchema>('/api/meta/admin/universities', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateUniversity(id: number, data: Types.MetaOptionWriteSchema) {
return this.request<Types.MetaOptionSchema>(`/api/meta/admin/universities/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteUniversity(id: number) {
return this.request<Types.MessageSchema>(`/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<Types.NotificationListSchema>(
`/api/notifications/${query.toString() ? `?${query.toString()}` : ''}`
);
}
async markNotificationSeen(id: string) {
return this.request<Types.NotificationSeenResponseSchema>('/api/notifications/mark-seen', {
method: 'POST',
body: JSON.stringify({ id }),
});
}
async deleteNotification(id: string) {
return this.request<Types.NotificationDeleteResponseSchema>(`/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<Types.NotificationStreamTokenResponseSchema>('/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);