feat(frontend): add blog editor and interactions
Some checks failed
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-06-08 21:31:07 +03:30
parent f2b4cfce1a
commit 49dcb1dd1b
10 changed files with 1019 additions and 35 deletions

View File

@@ -426,19 +426,90 @@ class ApiClient {
}
async createPost(data: Types.PostCreateSchema) {
return this.request<Types.PostDetailSchema>('/api/blog/posts', {
return this.request<Types.PostDetailSchema>('/api/blog/admin/posts', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updatePost(slug: string, data: Types.PostCreateSchema) {
return this.request<Types.PostDetailSchema>(`/api/blog/posts/${slug}`, {
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 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 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>;
}
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/${slug}`, {
method: 'DELETE',
@@ -467,6 +538,13 @@ class ApiClient {
});
}
async hideComment(commentId: number, note?: string) {
return this.request<Types.MessageSchema>(`/api/blog/comments/${commentId}/hide`, {
method: 'POST',
body: JSON.stringify({ note: note ?? '' }),
});
}
async listDeletedComments() {
return this.request<Types.CommentSchema[]>('/api/blog/deleted/comments');
}
@@ -479,15 +557,29 @@ class ApiClient {
// Likes
async toggleLike(slug: string) {
return this.request<Types.MessageSchema>(`/api/blog/posts/${slug}/like`, {
return this.request<Types.BlogInteractionSchema>(`/api/blog/posts/${slug}/like`, {
method: 'POST',
});
}
async toggleSave(slug: string) {
return this.request<Types.BlogInteractionSchema>(`/api/blog/posts/${slug}/save`, {
method: 'POST',
});
}
async getBlogInteraction(slug: string) {
return this.request<Types.BlogInteractionSchema>(`/api/blog/posts/${slug}/interaction`);
}
async getLikesCount(slug: string) {
return this.request<Types.MessageSchema>(`/api/blog/posts/${slug}/likes`);
}
async getMyBlogActivity() {
return this.request<Types.BlogProfileActivitySchema>('/api/blog/me/activity');
}
// Categories
async getCategories() {
return this.request<Types.CategorySchema[]>('/api/blog/categories');