const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1'; // Debug logging console.log('API_BASE_URL:', API_BASE_URL); console.log('VITE_API_URL env:', import.meta.env.VITE_API_URL); // Helper function to get auth headers function getAuthHeaders(): HeadersInit { const token = localStorage.getItem('token'); const headers: HeadersInit = { 'Content-Type': 'application/json', }; if (token) { headers['Authorization'] = `Bearer ${token}`; } return headers; } // Enhanced fetch wrapper async function authFetch(url: string, options: RequestInit = {}): Promise { const headers = getAuthHeaders(); const response = await fetch(url, { ...options, headers: { ...headers, ...options.headers, }, }); // Handle 401 unauthorized if (response.status === 401) { localStorage.removeItem('token'); localStorage.removeItem('user'); window.location.href = '/auth'; } return response; } export interface Article { id: string; title: string; content: string; excerpt: string | null; slug: string; featuredImage: string; imagePosition: 'top' | 'left' | 'right' | 'none'; imageSize: 'small' | 'medium' | 'large'; videoUrl: string; videoPosition: 'top' | 'inline' | 'bottom' | 'none'; videoCaption: string; tags: string[]; status: 'draft' | 'published' | 'archived'; views: number; strapiId: string | null; authorId: string | null; categoryId: string | null; author?: { id: string; name: string; slug: string; bio: string | null; avatar: string | null; }; category?: { id: string; name: string; slug: string; description: string | null; }; createdAt: string; updatedAt: string; isHero?: boolean; isPinned?: boolean; facebookShares?: number; twitterShares?: number; whatsappShares?: number; telegramShares?: number; ogTitle?: string; ogDescription?: string; ogImage?: string; twitterTitle?: string; twitterDescription?: string; twitterImage?: string; } export interface ArticlesResponse { data: Article[]; total: number; page?: number; limit?: number; } export interface FindArticlesParams { category?: string; author?: string; tag?: string; status?: string; search?: string; page?: number; limit?: number; } export interface CreateArticleDto { title: string; content: string; excerpt?: string; slug?: string; featuredImage?: string; tags?: string[]; status?: 'draft' | 'published' | 'archived'; strapiId?: string; authorId?: string; categoryId?: string; imagePosition?: 'top' | 'left' | 'right' | 'none'; imageSize?: 'small' | 'medium' | 'large'; videoUrl?: string; videoPosition?: 'top' | 'inline' | 'bottom' | 'none'; videoCaption?: string; } export interface UpdateArticleDto { title?: string; content?: string; excerpt?: string; slug?: string; featuredImage?: string; tags?: string[]; status?: 'draft' | 'published' | 'archived'; strapiId?: string; authorId?: string; categoryId?: string; imagePosition?: 'top' | 'left' | 'right' | 'none'; imageSize?: 'small' | 'medium' | 'large'; videoUrl?: string; videoPosition?: 'top' | 'inline' | 'bottom' | 'none'; videoCaption?: string; isHero?: boolean; } export async function fetchArticles(params: FindArticlesParams = {}): Promise { console.log('fetchArticles called with params:', params, 'API_BASE_URL:', API_BASE_URL); const searchParams = new URLSearchParams(); // Convert parameters to proper types for URLSearchParams Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (typeof value === 'number') { searchParams.append(key, value.toString()); } else { searchParams.append(key, String(value)); } } }); const url = `${API_BASE_URL}/articles?${searchParams}`; console.log('Fetching from:', url); const response = await authFetch(url); console.log('Response status:', response.status, 'ok:', response.ok); if (!response.ok) { throw new Error('Failed to fetch articles'); } const data = await response.json(); console.log('Response data:', data); return data; } export async function fetchArticleBySlug(slug: string): Promise
{ const response = await authFetch(`${API_BASE_URL}/articles/slug/${slug}`); if (!response.ok) { throw new Error('Failed to fetch article'); } return response.json(); } export async function fetchArticleById(id: string): Promise
{ const response = await authFetch(`${API_BASE_URL}/articles/${id}`); if (!response.ok) { throw new Error('Failed to fetch article'); } return response.json(); } export async function createArticle(dto: CreateArticleDto): Promise
{ const response = await authFetch(`${API_BASE_URL}/articles`, { method: 'POST', body: JSON.stringify(dto), }); if (!response.ok) { throw new Error('Failed to create article'); } return response.json(); } export async function updateArticle(id: string, dto: UpdateArticleDto): Promise
{ const response = await authFetch(`${API_BASE_URL}/articles/${id}`, { method: 'PUT', body: JSON.stringify(dto), }); if (!response.ok) { throw new Error('Failed to update article'); } return response.json(); } export async function deleteArticle(id: string): Promise { const response = await authFetch(`${API_BASE_URL}/articles/${id}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('Failed to delete article'); } } export async function archiveArticle(id: string): Promise
{ const response = await authFetch(`${API_BASE_URL}/articles/${id}/archive`, { method: 'PATCH', }); if (!response.ok) { throw new Error('Failed to archive article'); } return response.json(); } export async function publishArticle(id: string, status: 'draft' | 'published' = 'published'): Promise
{ const response = await authFetch(`${API_BASE_URL}/articles/${id}/publish?status=${status}`, { method: 'PATCH', }); if (!response.ok) { throw new Error('Failed to publish article'); } return response.json(); } // Live Blog Types export interface LiveBlogUpdate { id: string; content: string; isPinned: boolean; authorId: string | null; scheduledAt: string | null; strapiId: string | null; author?: { id: string; name: string; slug: string; bio: string | null; avatar: string | null; }; createdAt: string; updatedAt: string; } export interface LiveBlog { id: string; title: string; slug: string; description: string | null; status: 'draft' | 'live' | 'ended' | 'archived'; isPinned: boolean; strapiId: string | null; authorId: string | null; categoryId: string | null; featuredImage: string; imagePosition: 'top' | 'left' | 'right' | 'none'; imageSize: 'small' | 'medium' | 'large'; videoUrl: string; videoPosition: 'top' | 'inline' | 'bottom' | 'none'; videoCaption: string; viewCount: number; author?: { id: string; name: string; slug: string; bio: string | null; avatar: string | null; }; category?: { id: string; name: string; slug: string; description: string | null; }; updates?: LiveBlogUpdate[]; createdAt: string; updatedAt: string; } export interface LiveBlogsResponse { data: LiveBlog[]; total: number; page?: number; limit?: number; } export interface LiveBlogUpdatesResponse { data: LiveBlogUpdate[]; total: number; page?: number; limit?: number; } export interface FindLiveBlogsParams { category?: string; author?: string; status?: string; search?: string; page?: number; limit?: number; } export interface CreateLiveBlogUpdateDto { content: string; isPinned?: boolean; authorId?: string; scheduledAt?: string; strapiId?: string; } export interface UpdateLiveBlogUpdateDto { content?: string; isPinned?: boolean; authorId?: string; scheduledAt?: string; strapiId?: string; } export interface CreateLiveBlogDto { title: string; slug?: string; description?: string; status?: 'draft' | 'live' | 'ended' | 'archived'; authorId?: string; categoryId?: string; strapiId?: string; } export interface UpdateLiveBlogDto { title?: string; slug?: string; description?: string; status?: 'draft' | 'live' | 'ended' | 'archived'; isPinned?: boolean; authorId?: string; categoryId?: string; strapiId?: string; } // Live Blog API Functions export async function fetchLiveBlogs(params: FindLiveBlogsParams = {}): Promise { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (typeof value === 'number') { searchParams.append(key, value.toString()); } else { searchParams.append(key, String(value)); } } }); const response = await authFetch(`${API_BASE_URL}/live-blogs?${searchParams}`); if (!response.ok) { throw new Error('Failed to fetch live blogs'); } return response.json(); } export async function fetchLiveBlogBySlug(slugOrId: string): Promise { const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(slugOrId); const endpoint = isUuid ? `${API_BASE_URL}/live-blogs/${slugOrId}` : `${API_BASE_URL}/live-blogs/slug/${slugOrId}`; const response = await authFetch(endpoint); if (!response.ok) { throw new Error('Failed to fetch live blog'); } return response.json(); } export async function fetchLiveBlogById(id: string): Promise { const response = await authFetch(`${API_BASE_URL}/live-blogs/${id}`); if (!response.ok) { throw new Error('Failed to fetch live blog'); } return response.json(); } export async function fetchLiveBlogUpdates( liveBlogId: string, page = 1, limit = 50 ): Promise { const response = await authFetch( `${API_BASE_URL}/live-blogs/${liveBlogId}/updates?page=${page}&limit=${limit}` ); if (!response.ok) { throw new Error('Failed to fetch live blog updates'); } return response.json(); } export async function fetchRecentLiveBlogs(): Promise { const response = await authFetch(`${API_BASE_URL}/live-blogs/recent`); if (!response.ok) { throw new Error('Failed to fetch recent live blogs'); } return response.json(); } export async function fetchHeroArticle(): Promise
{ const response = await authFetch(`${API_BASE_URL}/articles/hero`); if (!response.ok) { throw new Error('Failed to fetch hero article'); } const article = await response.json(); return article || null; } export async function fetchLatestArticles(limit = 12): Promise { const response = await authFetch(`${API_BASE_URL}/articles?status=published&limit=${limit}`); if (!response.ok) { throw new Error('Failed to fetch latest articles'); } return response.json(); } export async function fetchPinnedLiveBlogs(): Promise { const response = await authFetch(`${API_BASE_URL}/live-blogs/featured`); if (!response.ok) { throw new Error('Failed to fetch pinned live blogs'); } return response.json(); } export async function fetchActiveLiveBlogs(): Promise { const response = await authFetch(`${API_BASE_URL}/live-blogs/active`); if (!response.ok) { throw new Error('Failed to fetch active live blogs'); } return response.json(); } // Admin functions export async function createLiveBlogUpdate( liveBlogId: string, dto: CreateLiveBlogUpdateDto ): Promise { const response = await authFetch(`${API_BASE_URL}/live-blogs/${liveBlogId}/updates`, { method: 'POST', body: JSON.stringify(dto), }); if (!response.ok) { throw new Error('Failed to create live blog update'); } return response.json(); } export async function updateLiveBlogUpdate( liveBlogId: string, updateId: string, dto: UpdateLiveBlogUpdateDto ): Promise { const response = await authFetch(`${API_BASE_URL}/live-blogs/${liveBlogId}/updates/${updateId}`, { method: 'PUT', body: JSON.stringify(dto), }); if (!response.ok) { throw new Error('Failed to update live blog update'); } return response.json(); } export async function deleteLiveBlogUpdate(liveBlogId: string, updateId: string): Promise { const response = await authFetch(`${API_BASE_URL}/live-blogs/${liveBlogId}/updates/${updateId}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('Failed to delete live blog update'); } } export async function createLiveBlog(dto: CreateLiveBlogDto): Promise { const response = await authFetch(`${API_BASE_URL}/live-blogs`, { method: 'POST', body: JSON.stringify(dto), }); if (!response.ok) { throw new Error('Failed to create live blog'); } return response.json(); } export async function updateLiveBlog(id: string, dto: UpdateLiveBlogDto): Promise { const response = await authFetch(`${API_BASE_URL}/live-blogs/${id}`, { method: 'PUT', body: JSON.stringify(dto), }); if (!response.ok) { throw new Error('Failed to update live blog'); } return response.json(); } export async function deleteLiveBlog(id: string): Promise { const response = await authFetch(`${API_BASE_URL}/live-blogs/${id}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('Failed to delete live blog'); } } export async function archiveLiveBlog(id: string): Promise { const response = await authFetch(`${API_BASE_URL}/live-blogs/${id}/archive`, { method: 'PATCH', }); if (!response.ok) { throw new Error('Failed to archive live blog'); } return response.json(); } export async function publishLiveBlog(id: string, status: 'draft' | 'live' | 'ended' = 'draft'): Promise { const response = await authFetch(`${API_BASE_URL}/live-blogs/${id}/publish?status=${status}`, { method: 'PATCH', }); if (!response.ok) { throw new Error('Failed to publish live blog'); } return response.json(); } // Auth Types import type { User, LoginDto, RegisterDto, AuthResponse } from '@/types'; // Auth API Functions export async function login(dto: LoginDto): Promise { const response = await fetch(`${API_BASE_URL}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(dto), }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Login failed'); } return response.json(); } export async function register(dto: RegisterDto): Promise { const response = await fetch(`${API_BASE_URL}/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(dto), }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Registration failed'); } return response.json(); } export async function getProfile(): Promise { const response = await authFetch(`${API_BASE_URL}/users/profile`, { method: 'GET', }); if (!response.ok) { throw new Error('Failed to fetch profile'); } return response.json(); } export async function logout(): Promise { localStorage.removeItem('token'); localStorage.removeItem('user'); } // Comment Types export interface Comment { id: string; content: string; articleId: string | null; liveBlogId: string | null; parentCommentId: string | null; userId: string; isVisible: boolean; user?: { id: string; username: string; email: string; role: string; }; reactions?: { likes: number; dislikes: number; }; replies?: Comment[]; createdAt: string; updatedAt: string; } export interface CreateCommentDto { content: string; articleId?: string; liveBlogId?: string; parentCommentId?: string; } export interface UpdateCommentDto { content?: string; isVisible?: boolean; } export interface FindCommentsParams { articleId?: string; liveBlogId?: string; parentCommentId?: string; parentId?: string; page?: number; limit?: number; } export interface CommentsResponse { data: Comment[]; total: number; page?: number; limit?: number; } export interface ReactionCounts { likes: number; dislikes: number; } export interface CreateReactionDto { type: 'like' | 'dislike'; articleId?: string; liveBlogId?: string; commentId?: string; } // Comment API Functions // Interface for backend comment response interface BackendComment { id: string; content: string; articleId: string | null; liveBlogId: string | null; parentId: string | null; userId: string; likeCount: number; dislikeCount: number; isVisible: boolean; createdAt: string; updatedAt: string; user?: { id: string; username: string; }; replies?: BackendComment[]; } // Recursive function to map comment and its replies function mapBackendComment(comment: BackendComment): Comment { const mappedComment: Comment = { id: comment.id, content: comment.content, articleId: comment.articleId, liveBlogId: comment.liveBlogId, parentCommentId: comment.parentId, userId: comment.userId, isVisible: comment.isVisible, reactions: { likes: comment.likeCount || 0, dislikes: comment.dislikeCount || 0, }, replies: comment.replies?.map(mapBackendComment) || [], createdAt: comment.createdAt, updatedAt: comment.updatedAt, }; return mappedComment; } export async function fetchComments(params: FindCommentsParams = {}): Promise { const searchParams = new URLSearchParams(); // Map parentCommentId to parentId for backend compatibility const backendParams = { ...params }; if (backendParams.parentCommentId) { backendParams.parentId = backendParams.parentCommentId; delete backendParams.parentCommentId; } Object.entries(backendParams).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (typeof value === 'number') { searchParams.append(key, value.toString()); } else { searchParams.append(key, String(value)); } } }); const response = await authFetch(`${API_BASE_URL}/comments?${searchParams}`); if (!response.ok) { throw new Error('Failed to fetch comments'); } const data = await response.json(); const mappedData = (data as BackendComment[]).map(mapBackendComment); return { data: mappedData, total: mappedData.length, }; } export async function createComment(dto: CreateCommentDto): Promise { // Map parentCommentId to parentId for backend compatibility const backendDto = { content: dto.content, articleId: dto.articleId, liveBlogId: dto.liveBlogId, parentId: dto.parentCommentId, }; const response = await authFetch(`${API_BASE_URL}/comments`, { method: 'POST', body: JSON.stringify(backendDto), }); if (!response.ok) { throw new Error('Failed to create comment'); } const comment = await response.json() as BackendComment; // Map backend response to frontend interface return mapBackendComment(comment); } export async function updateComment(id: string, dto: UpdateCommentDto): Promise { const response = await authFetch(`${API_BASE_URL}/comments/${id}`, { method: 'PUT', body: JSON.stringify(dto), }); if (!response.ok) { throw new Error('Failed to update comment'); } return response.json(); } export async function deleteComment(id: string): Promise { const response = await authFetch(`${API_BASE_URL}/comments/${id}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('Failed to delete comment'); } } export async function addReaction(dto: CreateReactionDto): Promise { const response = await authFetch(`${API_BASE_URL}/comments/reactions`, { method: 'POST', body: JSON.stringify(dto), }); if (!response.ok) { throw new Error('Failed to add reaction'); } } export async function getReactionCounts( articleId?: string, liveBlogId?: string, commentId?: string ): Promise { const searchParams = new URLSearchParams(); if (articleId) searchParams.append('articleId', articleId); if (liveBlogId) searchParams.append('liveBlogId', liveBlogId); if (commentId) searchParams.append('commentId', commentId); const url = `${API_BASE_URL}/comments/reactions/counts${searchParams.toString() ? `?${searchParams}` : ''}`; const response = await authFetch(url); if (!response.ok) { throw new Error('Failed to fetch reaction counts'); } return response.json(); } export async function getUserReaction( articleId?: string, liveBlogId?: string, commentId?: string ): Promise<{ type: string | null }> { const searchParams = new URLSearchParams(); if (articleId) searchParams.append('articleId', articleId); if (liveBlogId) searchParams.append('liveBlogId', liveBlogId); if (commentId) searchParams.append('commentId', commentId); const url = `${API_BASE_URL}/comments/reactions/user${searchParams.toString() ? `?${searchParams}` : ''}`; const response = await authFetch(url); if (!response.ok) { if (response.status === 401) { return { type: null }; } throw new Error('Failed to fetch user reaction'); } return response.json(); }