679 lines
25 KiB
TypeScript
679 lines
25 KiB
TypeScript
'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<BlogPostStatus, string> = {
|
|
draft: 'Brouillon',
|
|
published: 'Publié',
|
|
archived: 'Archivé',
|
|
};
|
|
|
|
const STATUS_COLORS: Record<BlogPostStatus, string> = {
|
|
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<BlogPost[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [selectedPost, setSelectedPost] = useState<BlogPost | null>(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<BlogPostStatus>('draft');
|
|
const [formData, setFormData] = useState<CreateBlogPostRequest>(EMPTY_FORM);
|
|
const coverInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
|
|
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 (
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<PageHeader
|
|
title="Gestion du Blog"
|
|
description="Créez, modifiez et publiez les articles du blog"
|
|
actions={
|
|
<button
|
|
onClick={openCreate}
|
|
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Nouvel article</span>
|
|
</button>
|
|
}
|
|
/>
|
|
|
|
{error && (
|
|
<div className="mb-6 p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="text-center py-12 text-gray-500">Chargement des articles...</div>
|
|
) : (
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mt-6">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Article
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Catégorie
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Statut
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Auteur
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Date
|
|
</th>
|
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-200">
|
|
{posts.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={6} className="px-6 py-12 text-center text-gray-500">
|
|
Aucun article. Créez votre premier article !
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
posts.map(post => (
|
|
<tr key={post.id} className="hover:bg-gray-50 transition-colors">
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-start space-x-3">
|
|
{post.coverImageUrl && (
|
|
<img
|
|
src={post.coverImageUrl}
|
|
alt=""
|
|
className="w-12 h-12 object-cover rounded flex-shrink-0"
|
|
/>
|
|
)}
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
{post.isFeatured && (
|
|
<Star className="w-3.5 h-3.5 text-yellow-500 flex-shrink-0" />
|
|
)}
|
|
<span className="font-medium text-gray-900 line-clamp-1">
|
|
{post.title}
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-gray-400 font-mono mt-0.5">{post.slug}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-gray-600">
|
|
{CATEGORIES.find(c => c.value === post.category)?.label ?? post.category}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<span
|
|
className={`px-2 py-1 rounded-full text-xs font-medium ${STATUS_COLORS[post.status]}`}
|
|
>
|
|
{STATUS_LABELS[post.status]}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-gray-600">{post.authorName}</td>
|
|
<td className="px-6 py-4 text-sm text-gray-500">
|
|
{post.publishedAt
|
|
? new Date(post.publishedAt).toLocaleDateString('fr-FR')
|
|
: new Date(post.createdAt).toLocaleDateString('fr-FR')}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center justify-end space-x-1">
|
|
{post.status === 'published' && (
|
|
<Link href={`/blog/${post.slug}`} target="_blank">
|
|
<button
|
|
className="p-1.5 text-gray-400 hover:text-blue-600 transition-colors"
|
|
title="Voir"
|
|
>
|
|
<ExternalLink className="w-4 h-4" />
|
|
</button>
|
|
</Link>
|
|
)}
|
|
<button
|
|
onClick={() => handleToggleFeatured(post)}
|
|
className="p-1.5 text-gray-400 hover:text-yellow-500 transition-colors"
|
|
title={post.isFeatured ? 'Retirer de la une' : 'Mettre à la une'}
|
|
>
|
|
{post.isFeatured ? (
|
|
<StarOff className="w-4 h-4" />
|
|
) : (
|
|
<Star className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => handleToggleStatus(post)}
|
|
className="p-1.5 text-gray-400 hover:text-green-600 transition-colors"
|
|
title={post.status === 'published' ? 'Dépublier' : 'Publier'}
|
|
>
|
|
{post.status === 'published' ? (
|
|
<EyeOff className="w-4 h-4" />
|
|
) : (
|
|
<Eye className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => openEdit(post)}
|
|
className="p-1.5 text-gray-400 hover:text-blue-600 transition-colors"
|
|
title="Modifier"
|
|
>
|
|
<Pencil className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setSelectedPost(post);
|
|
setShowDeleteConfirm(true);
|
|
}}
|
|
className="p-1.5 text-gray-400 hover:text-red-600 transition-colors"
|
|
title="Supprimer"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{/* Create / Edit Modal — Full-screen overlay */}
|
|
{isOpen && (
|
|
<div className="fixed inset-0 z-50 bg-gray-50 overflow-y-auto">
|
|
{/* Header */}
|
|
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between shadow-sm">
|
|
<h2 className="text-lg font-semibold text-gray-900">
|
|
{showCreateModal ? 'Nouvel article' : `Modifier — ${selectedPost?.title}`}
|
|
</h2>
|
|
<div className="flex items-center gap-3">
|
|
{showEditModal && (
|
|
<select
|
|
value={editStatus}
|
|
onChange={e => setEditStatus(e.target.value as BlogPostStatus)}
|
|
className="text-sm border border-gray-300 rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
>
|
|
<option value="draft">Brouillon</option>
|
|
<option value="published">Publié</option>
|
|
<option value="archived">Archivé</option>
|
|
</select>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={closeModal}
|
|
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<form
|
|
onSubmit={showCreateModal ? handleCreate : handleUpdate}
|
|
className="max-w-6xl mx-auto px-6 py-8 grid grid-cols-1 lg:grid-cols-3 gap-8"
|
|
>
|
|
{/* Main editor — 2/3 */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
{/* Title */}
|
|
<div>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.title}
|
|
onChange={e => 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..."
|
|
/>
|
|
</div>
|
|
|
|
{/* Excerpt */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Extrait *</label>
|
|
<textarea
|
|
required
|
|
rows={2}
|
|
value={formData.excerpt}
|
|
onChange={e => setFormData(prev => ({ ...prev, excerpt: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none text-sm resize-none"
|
|
placeholder="Courte description visible dans la liste des articles..."
|
|
/>
|
|
</div>
|
|
|
|
{/* Rich Text Editor */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Contenu *</label>
|
|
<RichTextEditor
|
|
content={formData.content}
|
|
onChange={html => setFormData(prev => ({ ...prev, content: html }))}
|
|
placeholder="Commencez à écrire votre article..."
|
|
minHeight={500}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar — 1/3 */}
|
|
<div className="space-y-5">
|
|
{/* Publish button */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-5 shadow-sm">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Publication</h3>
|
|
<button
|
|
type="submit"
|
|
disabled={saving}
|
|
className="w-full py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50 flex items-center justify-center gap-2"
|
|
>
|
|
{saving && <Loader2 className="w-4 h-4 animate-spin" />}
|
|
{saving
|
|
? 'Enregistrement...'
|
|
: showCreateModal
|
|
? 'Créer le brouillon'
|
|
: 'Enregistrer'}
|
|
</button>
|
|
{showCreateModal && (
|
|
<p className="text-xs text-gray-400 text-center mt-2">
|
|
Créé en brouillon. Publiez depuis la liste.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Cover image */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-5 shadow-sm">
|
|
<h3 className="font-semibold text-gray-900 mb-3">Image de couverture</h3>
|
|
{formData.coverImageUrl ? (
|
|
<div className="relative">
|
|
<img
|
|
src={formData.coverImageUrl}
|
|
alt="Couverture"
|
|
className="w-full h-40 object-cover rounded-lg"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setFormData(prev => ({ ...prev, coverImageUrl: '' }))}
|
|
className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-md text-gray-600 hover:text-red-600 transition-colors"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={() => coverInputRef.current?.click()}
|
|
disabled={uploadingCover}
|
|
className="w-full h-32 border-2 border-dashed border-gray-300 rounded-lg flex flex-col items-center justify-center gap-2 text-gray-400 hover:border-blue-400 hover:text-blue-500 transition-colors disabled:opacity-50"
|
|
>
|
|
{uploadingCover ? (
|
|
<Loader2 className="w-6 h-6 animate-spin" />
|
|
) : (
|
|
<>
|
|
<ImageIcon className="w-6 h-6" />
|
|
<span className="text-sm">Cliquer pour uploader</span>
|
|
<span className="text-xs">JPG, PNG, WebP — max 5 Mo</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
<input
|
|
ref={coverInputRef}
|
|
type="file"
|
|
accept="image/jpeg,image/png,image/webp,image/gif"
|
|
className="hidden"
|
|
onChange={handleCoverUpload}
|
|
/>
|
|
{/* Also allow URL */}
|
|
<div className="mt-3">
|
|
<input
|
|
type="url"
|
|
value={formData.coverImageUrl ?? ''}
|
|
onChange={e =>
|
|
setFormData(prev => ({ ...prev, coverImageUrl: e.target.value }))
|
|
}
|
|
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
placeholder="Ou coller une URL..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Metadata */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-5 shadow-sm space-y-4">
|
|
<h3 className="font-semibold text-gray-900">Métadonnées</h3>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Slug *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.slug}
|
|
onChange={e => setFormData(prev => ({ ...prev, slug: e.target.value }))}
|
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none font-mono"
|
|
placeholder="mon-article"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Catégorie *
|
|
</label>
|
|
<select
|
|
required
|
|
value={formData.category}
|
|
onChange={e =>
|
|
setFormData(prev => ({
|
|
...prev,
|
|
category: e.target.value as BlogPostCategory,
|
|
}))
|
|
}
|
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
>
|
|
{CATEGORIES.map(c => (
|
|
<option key={c.value} value={c.value}>
|
|
{c.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Auteur *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.authorName}
|
|
onChange={e => setFormData(prev => ({ ...prev, authorName: e.target.value }))}
|
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
placeholder="Nom de l'auteur"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tags <span className="text-gray-400 font-normal">(virgule)</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={tagsInput}
|
|
onChange={e => setTagsInput(e.target.value)}
|
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
placeholder="Maritime, LCL, Export"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* Delete Confirm */}
|
|
{showDeleteConfirm && selectedPost && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
|
<div className="bg-white rounded-xl shadow-2xl w-full max-w-md p-6">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-2">Supprimer l'article</h2>
|
|
<p className="text-gray-600 mb-6">
|
|
Êtes-vous sûr de vouloir supprimer <strong>« {selectedPost.title} »</strong>{' '}
|
|
? Cette action est irréversible.
|
|
</p>
|
|
<div className="flex justify-end space-x-3">
|
|
<button
|
|
onClick={() => {
|
|
setShowDeleteConfirm(false);
|
|
setSelectedPost(null);
|
|
}}
|
|
className="px-4 py-2 text-sm text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
onClick={handleDelete}
|
|
className="px-6 py-2 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors font-medium"
|
|
>
|
|
Supprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|