'use client'; import { useState, useEffect, useRef } from 'react'; import { getAllBlogPosts, createBlogPost, updateBlogPost, deleteBlogPost, type CreateBlogPostRequest, type UpdateBlogPostRequest, } from '@/lib/api/admin'; import { upload } from '@/lib/api/client'; import type { BlogPost, BlogPostCategory, BlogPostStatus } from '@/lib/api/blog'; import { PageHeader } from '@/components/ui/PageHeader'; import { RichTextEditor } from '@/components/blog/RichTextEditor'; import { Plus, Pencil, Trash2, Eye, EyeOff, Star, StarOff, ExternalLink, ImageIcon, X, Loader2, } from 'lucide-react'; import { Link } from '@/i18n/navigation'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'; function normalizeImageUrl(url: string | null | undefined): string { if (!url) return ''; if (url.startsWith('http')) return url; return `${API_BASE_URL}${url}`; } function normalizeContentUrls(html: string): string { return html.replace(/src="(\/api\/v1\/blog\/images\/[^"]+)"/g, `src="${API_BASE_URL}$1"`); } const CATEGORIES: { value: BlogPostCategory; label: string }[] = [ { value: 'industry', label: 'Industrie' }, { value: 'technology', label: 'Technologie' }, { value: 'guides', label: 'Guides' }, { value: 'news', label: 'Actualités' }, ]; const STATUS_LABELS: Record = { draft: 'Brouillon', published: 'Publié', archived: 'Archivé', }; const STATUS_COLORS: Record = { draft: 'bg-yellow-100 text-yellow-800', published: 'bg-green-100 text-green-800', archived: 'bg-gray-100 text-gray-600', }; const EMPTY_FORM: CreateBlogPostRequest = { title: '', slug: '', excerpt: '', content: '', coverImageUrl: '', category: 'industry', tags: [], authorName: '', }; function generateSlug(title: string): string { return title .toLowerCase() .normalize('NFD') .replace(/[̀-ͯ]/g, '') .replace(/[^a-z0-9\s-]/g, '') .trim() .replace(/\s+/g, '-'); } export default function AdminBlogPage() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedPost, setSelectedPost] = useState(null); const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [saving, setSaving] = useState(false); const [uploadingCover, setUploadingCover] = useState(false); const [tagsInput, setTagsInput] = useState(''); const [editStatus, setEditStatus] = useState('draft'); const [formData, setFormData] = useState(EMPTY_FORM); const coverInputRef = useRef(null); useEffect(() => { fetchPosts(); }, []); const fetchPosts = async () => { try { setLoading(true); const res = await getAllBlogPosts(); setPosts(res.posts); setError(null); } catch (err: any) { setError(err.message || 'Erreur lors du chargement des articles'); } finally { setLoading(false); } }; const handleCreate = async (e: React.FormEvent) => { e.preventDefault(); setSaving(true); try { await createBlogPost({ ...formData, tags: tagsInput ? tagsInput .split(',') .map(t => t.trim()) .filter(Boolean) : [], }); await fetchPosts(); closeModal(); } catch (err: any) { alert(err.message || 'Erreur lors de la création'); } finally { setSaving(false); } }; const handleUpdate = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedPost) return; setSaving(true); try { const data: UpdateBlogPostRequest = { ...formData, status: editStatus, tags: tagsInput ? tagsInput .split(',') .map(t => t.trim()) .filter(Boolean) : [], }; await updateBlogPost(selectedPost.id, data); await fetchPosts(); closeModal(); } catch (err: any) { alert(err.message || 'Erreur lors de la mise à jour'); } finally { setSaving(false); } }; const handleDelete = async () => { if (!selectedPost) return; try { await deleteBlogPost(selectedPost.id); await fetchPosts(); setShowDeleteConfirm(false); setSelectedPost(null); } catch (err: any) { alert(err.message || 'Erreur lors de la suppression'); } }; const handleToggleStatus = async (post: BlogPost) => { const nextStatus: BlogPostStatus = post.status === 'published' ? 'draft' : 'published'; try { await updateBlogPost(post.id, { status: nextStatus }); await fetchPosts(); } catch (err: any) { alert(err.message || 'Erreur lors du changement de statut'); } }; const handleToggleFeatured = async (post: BlogPost) => { try { await updateBlogPost(post.id, { isFeatured: !post.isFeatured }); await fetchPosts(); } catch (err: any) { alert(err.message || 'Erreur lors du changement'); } }; const handleCoverUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (file.size > 5 * 1024 * 1024) { alert('Image trop volumineuse (max 5 Mo)'); return; } setUploadingCover(true); try { const fd = new FormData(); fd.append('image', file); const result = await upload<{ url: string; filename: string }>( '/api/v1/admin/blog/images', fd ); const coverUrl = result.url.startsWith('http') ? result.url : `${API_BASE_URL}${result.url}`; setFormData(prev => ({ ...prev, coverImageUrl: coverUrl })); } catch (err: any) { alert(err.message || "Erreur lors de l'upload"); } finally { setUploadingCover(false); if (coverInputRef.current) coverInputRef.current.value = ''; } }; const openCreate = () => { setFormData(EMPTY_FORM); setTagsInput(''); setEditStatus('draft'); setSelectedPost(null); setShowCreateModal(true); }; const openEdit = (post: BlogPost) => { setSelectedPost(post); setFormData({ title: post.title, slug: post.slug, excerpt: post.excerpt, content: normalizeContentUrls(post.content), coverImageUrl: normalizeImageUrl(post.coverImageUrl), category: post.category, tags: post.tags, authorName: post.authorName, }); setTagsInput(post.tags.join(', ')); setEditStatus(post.status); setShowEditModal(true); }; const closeModal = () => { setShowCreateModal(false); setShowEditModal(false); setSelectedPost(null); setFormData(EMPTY_FORM); setTagsInput(''); }; const handleTitleChange = (title: string) => { setFormData(prev => ({ ...prev, title, slug: prev.slug === generateSlug(prev.title) || prev.slug === '' ? generateSlug(title) : prev.slug, })); }; const isOpen = showCreateModal || showEditModal; return (
Nouvel article } /> {error && (
{error}
)} {loading ? (
Chargement des articles...
) : (
{posts.length === 0 ? ( ) : ( posts.map(post => ( )) )}
Article Catégorie Statut Auteur Date Actions
Aucun article. Créez votre premier article !
{post.coverImageUrl && ( )}
{post.isFeatured && ( )} {post.title}
{post.slug}
{CATEGORIES.find(c => c.value === post.category)?.label ?? post.category} {STATUS_LABELS[post.status]} {post.authorName} {post.publishedAt ? new Date(post.publishedAt).toLocaleDateString('fr-FR') : new Date(post.createdAt).toLocaleDateString('fr-FR')}
{post.status === 'published' && ( )}
)} {/* Create / Edit Modal — Full-screen overlay */} {isOpen && (
{/* Header */}

{showCreateModal ? 'Nouvel article' : `Modifier — ${selectedPost?.title}`}

{showEditModal && ( )}
{/* Body */}
{/* Main editor — 2/3 */}
{/* Title */}
handleTitleChange(e.target.value)} className="w-full text-3xl font-bold text-gray-900 border-0 border-b-2 border-gray-200 focus:border-blue-500 focus:outline-none pb-2 placeholder-gray-300 bg-transparent" placeholder="Titre de l'article..." />
{/* Excerpt */}