mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
upload
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
import api, { API_URL } from './api';
|
||||
import { getToken } from '../utils/auth';
|
||||
|
||||
const normalizeArticle = (raw: any): Article => {
|
||||
if (!raw) return raw;
|
||||
const id = raw.id ?? raw.ID ?? raw.article_id ?? raw.articleId;
|
||||
const category = raw.category ?? raw.Category;
|
||||
const author = raw.author ?? raw.Author;
|
||||
return {
|
||||
...(raw as Article),
|
||||
id,
|
||||
category,
|
||||
author,
|
||||
} as Article;
|
||||
};
|
||||
|
||||
export interface Article {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
image_url?: string;
|
||||
author?: { id: number; first_name?: string; last_name?: string; email: string };
|
||||
category_id?: number;
|
||||
published?: boolean;
|
||||
featured?: boolean;
|
||||
created_at?: string;
|
||||
slug?: string;
|
||||
seo_title?: string;
|
||||
seo_description?: string;
|
||||
og_image_url?: string;
|
||||
published_at?: string;
|
||||
updated_at?: string;
|
||||
estimated_read_minutes?: number;
|
||||
read_count?: number;
|
||||
view_count?: number;
|
||||
read_time?: number;
|
||||
gallery_album_id?: string;
|
||||
gallery_album_url?: string;
|
||||
gallery_photo_ids?: string[];
|
||||
youtube_video_id?: string;
|
||||
youtube_video_title?: string;
|
||||
youtube_video_url?: string;
|
||||
youtube_video_thumbnail?: string;
|
||||
}
|
||||
|
||||
// --- Article ⇄ Match link ---
|
||||
export interface ArticleMatchLink {
|
||||
article_id: number;
|
||||
external_match_id?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export async function getArticleMatchLink(articleId: number | string): Promise<ArticleMatchLink> {
|
||||
const res = await api.get<ArticleMatchLink>(`/articles/${articleId}/match-link`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function putArticleMatchLink(articleId: number | string, payload: { external_match_id: string; title?: string }): Promise<ArticleMatchLink> {
|
||||
const res = await api.post<ArticleMatchLink>(`/articles/${articleId}/match-link`, payload);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function deleteArticleMatchLink(articleId: number | string): Promise<{ ok: boolean }> {
|
||||
const res = await api.delete<{ ok: boolean }>(`/articles/${articleId}/match-link`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getFeaturedArticles(params: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
} = {}) {
|
||||
const res = await api.get<any>('/articles/featured', { params });
|
||||
const d = res.data || {};
|
||||
const itemsRaw: any[] = Array.isArray(d.items)
|
||||
? d.items
|
||||
: Array.isArray(d.data)
|
||||
? d.data
|
||||
: [];
|
||||
const items: Article[] = itemsRaw.map((item) => normalizeArticle(item));
|
||||
const total = typeof d.total === 'number' ? d.total : items.length;
|
||||
const page = typeof d.page === 'number' ? d.page : (params as any).page || 1;
|
||||
const page_size = typeof d.page_size === 'number' ? d.page_size : (params as any).page_size || items.length;
|
||||
return { data: items, total, page, page_size } as Paginated<Article>;
|
||||
}
|
||||
|
||||
export interface Paginated<T> {
|
||||
data: T[];
|
||||
page: number;
|
||||
page_size: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export async function getArticles(params: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
category_id?: number;
|
||||
published?: boolean;
|
||||
featured?: boolean;
|
||||
q?: string;
|
||||
slug?: string;
|
||||
} = {}) {
|
||||
// Backend returns shape: { items, total, page, page_size }
|
||||
// Normalize to { data, total, page, page_size } expected by the frontend.
|
||||
const res = await api.get<any>('/articles', { params });
|
||||
const d = res.data || {};
|
||||
const itemsRaw: any[] = Array.isArray(d.items)
|
||||
? d.items
|
||||
: Array.isArray(d.data)
|
||||
? d.data
|
||||
: [];
|
||||
const items: Article[] = itemsRaw.map((item) => normalizeArticle(item));
|
||||
const total = typeof d.total === 'number' ? d.total : (typeof d.count === 'number' ? d.count : items.length);
|
||||
const page = typeof d.page === 'number' ? d.page : (params as any).page || 1;
|
||||
const page_size = typeof d.page_size === 'number' ? d.page_size : (params as any).page_size || items.length;
|
||||
return { data: items, total, page, page_size } as Paginated<Article>;
|
||||
}
|
||||
|
||||
export async function getArticle(id: number | string) {
|
||||
const res = await api.get<Article>(`/articles/${id}`);
|
||||
return normalizeArticle(res.data);
|
||||
}
|
||||
|
||||
export interface CreateArticlePayload {
|
||||
title: string;
|
||||
content: string;
|
||||
category_id?: number;
|
||||
category_name?: string; // optional: backend will resolve/create category by name
|
||||
published?: boolean;
|
||||
image_url?: string;
|
||||
slug?: string;
|
||||
seo_title?: string;
|
||||
seo_description?: string;
|
||||
og_image_url?: string;
|
||||
featured?: boolean;
|
||||
gallery_album_id?: string;
|
||||
gallery_album_url?: string;
|
||||
gallery_photo_ids?: string[];
|
||||
youtube_video_id?: string;
|
||||
youtube_video_title?: string;
|
||||
youtube_video_url?: string;
|
||||
youtube_video_thumbnail?: string;
|
||||
}
|
||||
|
||||
export async function createArticle(payload: CreateArticlePayload) {
|
||||
const res = await api.post<Article>('/articles', payload);
|
||||
return normalizeArticle(res.data);
|
||||
}
|
||||
|
||||
export type UpdateArticlePayload = Partial<CreateArticlePayload>;
|
||||
|
||||
export async function updateArticle(id: number | string, payload: UpdateArticlePayload) {
|
||||
const res = await api.put<Article>(`/articles/${id}`, payload);
|
||||
return normalizeArticle(res.data);
|
||||
}
|
||||
|
||||
export async function deleteArticle(id: number | string) {
|
||||
const res = await api.delete<{ success: boolean }>(`/articles/${id}`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// Fetch by slug if the backend exposes /articles/slug/:slug.
|
||||
// Falls back to querying the list endpoint with ?slug= if needed.
|
||||
export async function getArticleBySlug(slug: string) {
|
||||
try {
|
||||
const res = await api.get<Article>(`/articles/slug/${encodeURIComponent(slug)}`);
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
// Fallback: attempt list query through normalized helper and return first match
|
||||
const list = await getArticles({ slug });
|
||||
return list.data?.[0];
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadFile(file: File) {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
// Important: Do NOT set Content-Type manually so Axios can attach the proper multipart boundary.
|
||||
// The api instance will automatically add Authorization via the interceptor when a token exists.
|
||||
const res = await api.post<{ url: string; name: string; type: string; size: number }>(
|
||||
'/upload',
|
||||
form
|
||||
);
|
||||
const data = res.data;
|
||||
let url = data.url || '';
|
||||
try {
|
||||
// Normalize URLs returned by backend: if the returned URL points to the frontend origin (dev server)
|
||||
// or to the API origin, convert it to a backend-relative path like '/uploads/2025/...'
|
||||
const parsed = new URL(url, window.location.origin);
|
||||
const appOrigin = window.location.origin;
|
||||
const apiOrigin = new URL(API_URL).origin;
|
||||
if (parsed.origin === appOrigin) {
|
||||
// Rewrite frontend-origin URLs to API origin, then strip origin to keep a relative path
|
||||
url = parsed.pathname + parsed.search + parsed.hash;
|
||||
} else if (parsed.origin === apiOrigin) {
|
||||
// Keep only the path so stored values are consistent (backend relative path)
|
||||
url = parsed.pathname + parsed.search + parsed.hash;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore parsing errors, keep original
|
||||
}
|
||||
return { ...data, url };
|
||||
}
|
||||
|
||||
export async function trackArticleView(id: number | string) {
|
||||
try {
|
||||
await api.post(`/articles/${id}/track-view`);
|
||||
} catch (e) {
|
||||
console.debug('Failed to track article view:', e);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user