import api, { API_URL } from './api'; import { getToken } from '../utils/auth'; const normalizeArticle = (raw: any): Article => { if (!raw) return raw as Article; const id = raw.id ?? raw.ID ?? raw.article_id ?? raw.articleId; const category = raw.category ?? raw.Category; const author = raw.author ?? raw.Author; // Normalize attachments: backend may send a JSON string or an array let attachments: Array<{ name: string; url: string; mime_type?: string; size?: number }> | undefined = undefined; const aRaw = raw.attachments ?? raw.Attachments; try { if (Array.isArray(aRaw)) { attachments = aRaw.map((it: any) => { if (typeof it === 'string') { const name = it.split('/').pop() || 'soubor'; return { name, url: it }; } return { name: it?.name || (String(it?.url || '').split('/').pop() || 'soubor'), url: it?.url || '', mime_type: it?.mime_type || it?.type, size: it?.size }; }); } else if (typeof aRaw === 'string' && aRaw.trim() !== '') { const parsed = JSON.parse(aRaw); if (Array.isArray(parsed)) { attachments = parsed.map((it: any) => { if (typeof it === 'string') { const name = it.split('/').pop() || 'soubor'; return { name, url: it }; } return { name: it?.name || (String(it?.url || '').split('/').pop() || 'soubor'), url: it?.url || '', mime_type: it?.mime_type || it?.type, size: it?.size }; }); } } } catch { // ignore malformed attachments } return { ...(raw as Article), id, category, author, ...(attachments ? { attachments } : {}), } 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; category?: { id: number; name: string; description?: string; slug?: string; created_at?: string; updated_at?: string }; category_name?: string; 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; attachments?: Array<{ name: string; url: string; mime_type?: string; size?: number }>; } // --- Article ⇄ Match link --- export interface ArticleMatchLink { article_id: number; external_match_id?: string; title?: string; } export async function getArticleMatchLink(articleId: number | string): Promise { const res = await api.get(`/articles/${articleId}/match-link`); return res.data; } export async function putArticleMatchLink(articleId: number | string, payload: { external_match_id: string; title?: string }): Promise { const res = await api.post(`/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('/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
; } export interface Paginated { 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; match_id?: string | number; month?: string; // YYYY-MM } = {}) { // Backend returns shape: { items, total, page, page_size } // Normalize to { data, total, page, page_size } expected by the frontend. const res = await api.get('/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
; } export async function getArticle(id: number | string) { const res = await api.get
(`/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; attachments?: Array<{ name: string; url: string; mime_type?: string; size?: number }>; } export async function createArticle(payload: CreateArticlePayload) { const res = await api.post
('/articles', payload); return normalizeArticle(res.data); } export type UpdateArticlePayload = Partial; export async function updateArticle(id: number | string, payload: UpdateArticlePayload) { const res = await api.put
(`/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
(`/articles/slug/${encodeURIComponent(slug)}`); return normalizeArticle(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 { // Send an explicit empty JSON body to satisfy backend Content-Type validation await api.post(`/articles/${id}/track-view`, {}); } catch (e) { console.debug('Failed to track article view:', e); } }