feat(frontend): add blog editor and interactions
This commit is contained in:
100
src/lib/api.ts
100
src/lib/api.ts
@@ -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');
|
||||
|
||||
@@ -47,6 +47,9 @@ export interface UserProfileSchema {
|
||||
is_committee?: boolean;
|
||||
is_deleted?: boolean;
|
||||
deleted_at?: string | null;
|
||||
can_access_blog_admin?: boolean;
|
||||
can_write_blog_posts?: boolean;
|
||||
can_review_blog_posts?: boolean;
|
||||
}
|
||||
|
||||
export interface UserListSchema {
|
||||
@@ -246,24 +249,70 @@ export interface PostListSchema {
|
||||
created_at: string;
|
||||
is_featured: boolean;
|
||||
reading_time?: number;
|
||||
updated_at: string;
|
||||
seo_title?: string;
|
||||
seo_description?: string;
|
||||
canonical_url?: string;
|
||||
og_title?: string;
|
||||
og_description?: string;
|
||||
noindex?: boolean;
|
||||
focus_keyword?: string;
|
||||
likes_count?: number;
|
||||
saves_count?: number;
|
||||
comments_count?: number;
|
||||
}
|
||||
|
||||
export interface PostDetailSchema extends PostListSchema {
|
||||
content: string;
|
||||
content_html?: string;
|
||||
updated_at: string;
|
||||
og_image_url?: string | null;
|
||||
views_count?: number;
|
||||
assets?: PostAssetSchema[];
|
||||
}
|
||||
|
||||
export interface PostCreateSchema {
|
||||
title: string;
|
||||
content: string;
|
||||
summary: string;
|
||||
category_id?: number;
|
||||
excerpt?: string;
|
||||
category_id?: number | null;
|
||||
tag_ids?: number[];
|
||||
featured_image?: string;
|
||||
is_featured?: boolean;
|
||||
status?: 'draft' | 'published';
|
||||
status?: 'draft' | 'submitted' | 'changes_requested' | 'published' | 'archived';
|
||||
seo_title?: string;
|
||||
seo_description?: string;
|
||||
canonical_url?: string;
|
||||
og_title?: string;
|
||||
og_description?: string;
|
||||
noindex?: boolean;
|
||||
focus_keyword?: string;
|
||||
}
|
||||
|
||||
export interface PostReviewSchema {
|
||||
action: 'publish' | 'approve' | 'request_changes' | 'changes_requested' | 'archive';
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface PostAssetSchema {
|
||||
id: number;
|
||||
file_type: 'image' | 'video' | 'document' | 'archive' | 'other';
|
||||
title: string;
|
||||
alt_text?: string;
|
||||
caption?: string;
|
||||
size: number;
|
||||
mime_type?: string;
|
||||
created_at: string;
|
||||
absolute_file_url?: string | null;
|
||||
absolute_thumbnail_url?: string | null;
|
||||
absolute_preview_url?: string | null;
|
||||
absolute_blur_url?: string | null;
|
||||
markdown_image?: string | null;
|
||||
markdown_link?: string | null;
|
||||
uploaded_by: {
|
||||
id: number;
|
||||
username: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CommentSchema {
|
||||
@@ -281,6 +330,8 @@ export interface CommentSchema {
|
||||
parent_id?: number;
|
||||
created_at: string;
|
||||
is_approved: boolean;
|
||||
hidden_at?: string | null;
|
||||
replies?: CommentSchema[];
|
||||
}
|
||||
|
||||
export interface CommentCreateSchema {
|
||||
@@ -288,6 +339,21 @@ export interface CommentCreateSchema {
|
||||
parent_id?: number;
|
||||
}
|
||||
|
||||
export interface BlogInteractionSchema {
|
||||
liked: boolean;
|
||||
saved: boolean;
|
||||
likes_count: number;
|
||||
saves_count: number;
|
||||
comments_count: number;
|
||||
}
|
||||
|
||||
export interface BlogProfileActivitySchema {
|
||||
liked_posts: PostListSchema[];
|
||||
saved_posts: PostListSchema[];
|
||||
comments: CommentSchema[];
|
||||
replies: CommentSchema[];
|
||||
}
|
||||
|
||||
export interface CategorySchema {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user