feat(frontend): add async admin form foundations
This commit is contained in:
186
src/lib/api.ts
186
src/lib/api.ts
@@ -397,6 +397,10 @@ class ApiClient {
|
||||
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');
|
||||
}
|
||||
@@ -856,6 +860,68 @@ class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
@@ -971,6 +1037,44 @@ class ApiClient {
|
||||
);
|
||||
}
|
||||
|
||||
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?: {
|
||||
@@ -1019,12 +1123,86 @@ class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
async getMajors(): Promise<Types.MajorOption[]> {
|
||||
return this.request('/api/meta/majors', { method: 'GET' });
|
||||
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(): Promise<Types.MajorOption[]> {
|
||||
return this.request('/api/meta/universities', { method: 'GET' });
|
||||
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) {
|
||||
|
||||
110
src/lib/types.ts
110
src/lib/types.ts
@@ -18,6 +18,27 @@ export interface TokenSchema {
|
||||
export interface MajorOption {
|
||||
code: string;
|
||||
label: string;
|
||||
id?: number;
|
||||
name?: string;
|
||||
user_count?: number;
|
||||
}
|
||||
|
||||
export interface MetaOptionSchema {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
label: string;
|
||||
user_count?: number;
|
||||
}
|
||||
|
||||
export interface PagedMetaOptionSchema {
|
||||
count: number;
|
||||
results: MetaOptionSchema[];
|
||||
}
|
||||
|
||||
export interface MetaOptionWriteSchema {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UserProfileSchema {
|
||||
@@ -62,6 +83,19 @@ export interface UserListSchema {
|
||||
full_name?: string | null;
|
||||
university?: string | null;
|
||||
major?: string | null;
|
||||
profile_picture?: string | null;
|
||||
profile_picture_thumbnail_url?: string | null;
|
||||
profile_picture_preview_url?: string | null;
|
||||
student_id?: string | null;
|
||||
year_of_study?: number | null;
|
||||
bio?: string | null;
|
||||
is_email_verified?: boolean;
|
||||
is_mobile_verified?: boolean;
|
||||
is_deleted?: boolean;
|
||||
deleted_at?: string | null;
|
||||
can_access_blog_admin?: boolean;
|
||||
can_write_blog_posts?: boolean;
|
||||
can_review_blog_posts?: boolean;
|
||||
is_active: boolean;
|
||||
is_staff: boolean;
|
||||
is_superuser: boolean;
|
||||
@@ -524,6 +558,12 @@ export interface EventGalleryItem {
|
||||
absolute_image_blur_url?: string | null;
|
||||
width?: number;
|
||||
height?: number;
|
||||
file_size_mb?: number;
|
||||
markdown_url?: string;
|
||||
image?: string;
|
||||
alt_text?: string | null;
|
||||
is_public?: boolean;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface EventDetailSchema extends EventListItemSchema {
|
||||
@@ -536,19 +576,28 @@ export interface EventDetailSchema extends EventListItemSchema {
|
||||
export interface EventCreateSchema {
|
||||
title: string;
|
||||
description: string;
|
||||
start_date: string;
|
||||
end_date?: string;
|
||||
location: string;
|
||||
capacity?: number;
|
||||
event_image?: string;
|
||||
requirements?: string;
|
||||
is_registration_open?: boolean;
|
||||
slug?: string | null;
|
||||
event_type: 'online' | 'on_site' | 'hybrid';
|
||||
address?: string | null;
|
||||
location?: string | null;
|
||||
online_link?: string | null;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
registration_start_date?: string | null;
|
||||
registration_end_date?: string | null;
|
||||
capacity?: number | null;
|
||||
price?: number | null;
|
||||
status?: 'draft' | 'published' | 'cancelled' | 'completed';
|
||||
registration_success_markdown?: string | null;
|
||||
gallery_image_ids?: number[] | null;
|
||||
}
|
||||
|
||||
export interface PaymentAdminSchema {
|
||||
id: number;
|
||||
authority?: string | null;
|
||||
ref_id?: string | null;
|
||||
card_pan?: string | null;
|
||||
card_hash?: string | null;
|
||||
status: number;
|
||||
status_label: string;
|
||||
base_amount: number;
|
||||
@@ -573,6 +622,14 @@ export interface RegistrationAdminSchema {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
mobile?: string | null;
|
||||
profile_picture?: string | null;
|
||||
profile_picture_thumbnail_url?: string | null;
|
||||
profile_picture_preview_url?: string | null;
|
||||
university?: string | null;
|
||||
major?: string | null;
|
||||
student_id?: string | null;
|
||||
year_of_study?: number | null;
|
||||
};
|
||||
payments: PaymentAdminSchema[];
|
||||
}
|
||||
@@ -582,6 +639,7 @@ export interface EventAdminDetailSchema extends EventDetailSchema {
|
||||
}
|
||||
export interface EventUpdateSchema {
|
||||
title?: string;
|
||||
slug?: string | null;
|
||||
description?: string;
|
||||
event_type?: 'online' | 'on_site' | 'hybrid';
|
||||
address?: string | null;
|
||||
@@ -594,9 +652,47 @@ export interface EventUpdateSchema {
|
||||
capacity?: number | null;
|
||||
price?: number | null;
|
||||
status?: 'draft' | 'published' | 'cancelled' | 'completed';
|
||||
registration_success_markdown?: string | null;
|
||||
gallery_image_ids?: number[] | null;
|
||||
}
|
||||
|
||||
export interface DiscountCodeSchema {
|
||||
id: number;
|
||||
code: string;
|
||||
type: 'percent' | 'fixed';
|
||||
value: number;
|
||||
max_discount?: number | null;
|
||||
is_active: boolean;
|
||||
starts_at?: string | null;
|
||||
ends_at?: string | null;
|
||||
usage_limit_total?: number | null;
|
||||
usage_limit_per_user?: number | null;
|
||||
min_amount?: number | null;
|
||||
applicable_event_ids: number[];
|
||||
usage_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface PagedDiscountCodeSchema {
|
||||
count: number;
|
||||
results: DiscountCodeSchema[];
|
||||
}
|
||||
|
||||
export interface DiscountCodeWriteSchema {
|
||||
code: string;
|
||||
type: 'percent' | 'fixed';
|
||||
value: number;
|
||||
max_discount?: number | null;
|
||||
is_active?: boolean;
|
||||
starts_at?: string | null;
|
||||
ends_at?: string | null;
|
||||
usage_limit_total?: number | null;
|
||||
usage_limit_per_user?: number | null;
|
||||
min_amount?: number | null;
|
||||
applicable_event_ids?: number[];
|
||||
}
|
||||
|
||||
export interface EventRegistrationSchema {
|
||||
id: number;
|
||||
status: 'pending' | 'confirmed' | 'cancelled' | 'attended';
|
||||
|
||||
Reference in New Issue
Block a user