feat: add JSON import functionality for content creation in the admin dashboard

This commit is contained in:
2026-05-17 13:23:11 +02:00
parent 8f0dc7f38c
commit 0054f731c7
10 changed files with 907 additions and 35 deletions

View File

@@ -3,7 +3,7 @@
import { useTransition, useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { createBlogPost, updateBlogPost } from '@/app/actions/actualites';
import { Loader2, ArrowLeft, Save, Calendar, CheckCircle2, FileText, Archive, Link as LinkIcon, Plus, Trash2 } from 'lucide-react';
import { Loader2, ArrowLeft, Save, Calendar, CheckCircle2, FileText, Archive, Link as LinkIcon, Plus, Trash2, FileJson } from 'lucide-react';
import Link from 'next/link';
import { toast } from 'react-hot-toast';
import RichTextEditor from './RichTextEditor';
@@ -35,6 +35,14 @@ interface Props {
export default function BlogForm({ initialData }: Props) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
// JSON prefill state
const [currentData, setCurrentData] = useState<any>(initialData || {});
const [jsonString, setJsonString] = useState('');
const [showJsonInput, setShowJsonInput] = useState(false);
const [jsonError, setJsonError] = useState<string | null>(null);
// Form input states
const [content, setContent] = useState(initialData?.content || '');
const [imageUrl, setImageUrl] = useState(initialData?.imageUrl || '');
const [coverPosition, setCoverPosition] = useState(initialData?.coverPosition || '50% 50%');
@@ -56,6 +64,48 @@ export default function BlogForm({ initialData }: Props) {
getAllTags().then(setAllExistingTags);
}, []);
const handleApplyJson = () => {
try {
if (!jsonString.trim()) {
throw new Error("Veuillez coller un code JSON.");
}
const parsed = JSON.parse(jsonString);
// Extract new combined tags
const newTags = Array.isArray(parsed.tags) ? [...parsed.tags] : [...tags];
if (parsed.type && !newTags.includes(parsed.type)) {
newTags.push(parsed.type);
}
// Update states to prefill rich components
if (parsed.content) setContent(parsed.content);
if (parsed.imageUrl) setImageUrl(parsed.imageUrl);
setTags(newTags);
if (Array.isArray(parsed.sources)) {
setSources(parsed.sources);
}
// Update currentData to reset defaultValues on native inputs via form key remount
setCurrentData({
...currentData,
title: parsed.title || currentData.title || '',
excerpt: parsed.excerpt || currentData.excerpt || '',
author: parsed.author || currentData.author || 'Rédaction Afrohub',
slug: parsed.slug || currentData.slug || '',
metaTitle: parsed.metaTitle || currentData.metaTitle || '',
metaDescription: parsed.metaDescription || currentData.metaDescription || '',
_prefillKey: Date.now() // Force form remount
});
setJsonError(null);
setShowJsonInput(false);
setJsonString('');
toast.success("Champs pré-remplis avec le JSON !");
} catch (err: any) {
setJsonError("Format JSON invalide. Vérifiez la syntaxe : " + (err.message || ''));
}
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
@@ -111,7 +161,63 @@ export default function BlogForm({ initialData }: Props) {
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-8">
{/* PREFILL JSON SECTION */}
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4 mb-8 shadow-md">
<button
type="button"
onClick={() => setShowJsonInput(!showJsonInput)}
className="flex items-center justify-between w-full text-left font-bold text-sm text-brand-400 hover:text-brand-300 transition-colors"
>
<span className="flex items-center gap-2">
<FileJson className="w-5 h-5 text-indigo-400" />
Importer JSON (Pré-remplir les champs automatiquement)
</span>
<span className="text-xs bg-slate-800 px-2.5 py-1 rounded text-slate-400 border border-slate-700">
{showJsonInput ? 'Masquer' : 'Déplier'}
</span>
</button>
{showJsonInput && (
<div className="mt-4 pt-4 border-t border-slate-800 space-y-3 animate-in fade-in duration-200">
<p className="text-xs text-slate-400">
Collez le code JSON généré par Gemini pour remplir instantanément le titre, l'extrait, le contenu HTML, l'auteur, l'image, le slug, les tags SEO, et les sources.
</p>
{jsonError && (
<div className="text-xs text-red-400 bg-red-500/10 border border-red-500/20 p-2.5 rounded-lg font-medium">
{jsonError}
</div>
)}
<textarea
value={jsonString}
onChange={(e) => setJsonString(e.target.value)}
placeholder={'{\n "title": "Les 5 tendances de la Tech Africaine en 2026",\n "excerpt": "Découvrez comment l\'intelligence artificielle...",\n "content": "<p>L\'année 2026 marque un tournant...</p>",\n "author": "Rédaction Afrohub",\n "imageUrl": "https://images.unsplash.com/photo-...",\n "slug": "tendances-tech-afrique-2026",\n "tags": ["Technologie", "IA", "FinTech"],\n "metaTitle": "Les 5 tendances Tech en Afrique - Afrohub",\n "metaDescription": "Analyse complète des tendances de l\'écosystème tech...",\n "sources": [\n { "title": "Rapport BAD 2026", "url": "https://afdb.org" }\n ]\n}'}
rows={8}
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 text-xs font-mono text-emerald-400 focus:outline-none focus:border-indigo-500"
spellCheck={false}
/>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => {
setJsonString('{\n "title": "Les 5 tendances de la Tech Africaine en 2026",\n "excerpt": "Découvrez comment l\'intelligence artificielle et la FinTech redéfinissent l\'écosystème entrepreneurial sur le continent cette année.",\n "content": "<p>L\'année 2026 marque un tournant décisif. Les startups...</p><h3>L\'IA au service de l\'agriculture</h3><p>De nouvelles solutions émergent...</p>",\n "author": "Rédaction Afrohub",\n "imageUrl": "https://images.unsplash.com/photo-1522071820081-009f0129c71c?auto=format&fit=crop&q=80&w=1200",\n "slug": "tendances-tech-afrique-2026",\n "tags": ["Technologie", "IA", "FinTech"],\n "metaTitle": "Les 5 tendances Tech en Afrique - Afrohub",\n "metaDescription": "Analyse complète des tendances de l\'écosystème tech et des financements sur le continent africain en 2026.",\n "sources": [\n { "title": "Rapport BAD 2026", "url": "https://afdb.org" },\n { "title": "Jeune Afrique", "url": "https://jeuneafrique.com" }\n ]\n}');
}}
className="px-3 py-1.5 text-xs text-slate-400 hover:text-white transition-colors"
>
Exemple complet
</button>
<button
type="button"
onClick={handleApplyJson}
className="px-4 py-1.5 text-xs font-bold bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors shadow-sm"
>
Appliquer les données
</button>
</div>
</div>
)}
</div>
<form key={currentData._prefillKey || 'form'} onSubmit={handleSubmit} className="space-y-8">
<div className="card space-y-6">
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
<h2 className="text-xl font-semibold text-white">Informations Générales</h2>
@@ -119,7 +225,7 @@ export default function BlogForm({ initialData }: Props) {
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Statut :</label>
<select
name="status"
defaultValue={initialData?.status || 'PUBLISHED'}
defaultValue={currentData.status || 'PUBLISHED'}
className="bg-slate-900 border border-slate-700 text-white text-xs rounded-full px-4 py-1.5 focus:outline-none focus:border-brand-500 appearance-none cursor-pointer hover:bg-slate-800 transition-colors"
>
<option value="DRAFT">Brouillon</option>
@@ -134,7 +240,7 @@ export default function BlogForm({ initialData }: Props) {
<label className="text-sm font-medium text-slate-400">Titre</label>
<input
name="title"
defaultValue={initialData?.title}
defaultValue={currentData.title}
required
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
placeholder="Ex: L'essor de la Tech en Afrique"
@@ -144,7 +250,7 @@ export default function BlogForm({ initialData }: Props) {
<label className="text-sm font-medium text-slate-400">Auteur</label>
<input
name="author"
defaultValue={initialData?.author}
defaultValue={currentData.author || (initialData ? undefined : 'Rédaction Afrohub')}
required
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
placeholder="Ex: Jean Dupont"
@@ -187,7 +293,7 @@ export default function BlogForm({ initialData }: Props) {
<label className="text-sm font-medium text-slate-400">Extrait (Excerpt)</label>
<textarea
name="excerpt"
defaultValue={initialData?.excerpt}
defaultValue={currentData.excerpt}
required
rows={2}
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
@@ -269,7 +375,7 @@ export default function BlogForm({ initialData }: Props) {
<label className="text-sm font-medium text-slate-400">Slug (URL personnalisée)</label>
<input
name="slug"
defaultValue={initialData?.slug || ''}
defaultValue={currentData.slug || ''}
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
placeholder="ex: lessor-de-la-tech-afrique"
/>
@@ -290,7 +396,7 @@ export default function BlogForm({ initialData }: Props) {
<label className="text-sm font-medium text-slate-400">Meta Titre (SEO)</label>
<input
name="metaTitle"
defaultValue={initialData?.metaTitle || ''}
defaultValue={currentData.metaTitle || ''}
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
placeholder="Titre optimisé pour les moteurs de recherche"
/>
@@ -300,7 +406,7 @@ export default function BlogForm({ initialData }: Props) {
<label className="text-sm font-medium text-slate-400">Meta Description (SEO)</label>
<textarea
name="metaDescription"
defaultValue={initialData?.metaDescription || ''}
defaultValue={currentData.metaDescription || ''}
rows={3}
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
placeholder="Description optimisée pour les moteurs de recherche (max 160 caractères)..."