From 0054f731c7b212d2a31a647852c8ad72d63b73fa Mon Sep 17 00:00:00 2001 From: streaper2 Date: Sun, 17 May 2026 13:23:11 +0200 Subject: [PATCH] feat: add JSON import functionality for content creation in the admin dashboard --- .env | 2 +- admin/src/app/actualites/import/page.tsx | 13 + admin/src/app/actualites/page.tsx | 2 + admin/src/app/api/external/articles/route.ts | 64 +++ admin/src/app/api/external/events/route.ts | 58 +++ admin/src/components/ActualitesForm.tsx | 124 +++++- admin/src/components/EventForm.tsx | 135 ++++++- admin/src/components/ImportJsonForm.tsx | 393 +++++++++++++++++++ admin/src/components/InterviewForm.tsx | 138 ++++++- check-posts.ts | 13 + 10 files changed, 907 insertions(+), 35 deletions(-) create mode 100644 admin/src/app/actualites/import/page.tsx create mode 100644 admin/src/app/api/external/articles/route.ts create mode 100644 admin/src/app/api/external/events/route.ts create mode 100644 admin/src/components/ImportJsonForm.tsx create mode 100644 check-posts.ts diff --git a/.env b/.env index 0946db5..f502447 100644 --- a/.env +++ b/.env @@ -5,4 +5,4 @@ DATABASE_URL="postgresql://admin:adminpassword@192.168.1.25:5432/afrohub_dev?sch # Exemple avec Supabase ou Neon (si base distante) # DATABASE_URL="postgresql://postgres:[MOT_DE_PASSE]@db.[ID_PROJET].supabase.co:5432/postgres" # Environnement : "development" = accents rouges, "production" = accents normaux -NEXT_PUBLIC_APP_ENV=development \ No newline at end of file +NEXT_PUBLIC_APP_ENV=development diff --git a/admin/src/app/actualites/import/page.tsx b/admin/src/app/actualites/import/page.tsx new file mode 100644 index 0000000..c8897c5 --- /dev/null +++ b/admin/src/app/actualites/import/page.tsx @@ -0,0 +1,13 @@ +import ImportJsonForm from '@/components/ImportJsonForm'; + +export const metadata = { + title: 'Import JSON - CMS Afrohub', +}; + +export default function ImportJsonPage() { + return ( +
+ +
+ ); +} diff --git a/admin/src/app/actualites/page.tsx b/admin/src/app/actualites/page.tsx index bfad321..d2e1890 100644 --- a/admin/src/app/actualites/page.tsx +++ b/admin/src/app/actualites/page.tsx @@ -5,6 +5,7 @@ import DeleteActualitesButton from '@/components/DeleteActualitesButton'; import DeleteInterviewButton from '@/components/DeleteInterviewButton'; import DeleteEventButton from '@/components/DeleteEventButton'; import ApproveContentButton from '@/components/ApproveContentButton'; +import { FileJson } from 'lucide-react'; async function getData() { const posts = await prisma.blogPost.findMany({ orderBy: { createdAt: 'desc' } }); @@ -50,6 +51,7 @@ export default async function ActualitesCMSPage() { Nouvel Événement + diff --git a/admin/src/app/api/external/articles/route.ts b/admin/src/app/api/external/articles/route.ts new file mode 100644 index 0000000..32ab683 --- /dev/null +++ b/admin/src/app/api/external/articles/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from 'next/server' +import prisma from '@/lib/prisma' +import { generateSlug } from '@/lib/utils' +import { revalidatePath } from 'next/cache' + +export async function POST(request: NextRequest) { + try { + // Simple API Key authentication + const apiKey = request.headers.get('x-api-key') + const expectedApiKey = process.env.EXTERNAL_API_KEY + + if (!expectedApiKey) { + console.error('EXTERNAL_API_KEY is not defined in environment variables.') + return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }) + } + + if (apiKey !== expectedApiKey) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + + // Validation + const { title, excerpt, content, author, imageUrl, type, tags, status, publishedAt } = body + + if (!title || !excerpt || !content || !author || !imageUrl) { + return NextResponse.json({ + error: 'Missing required fields: title, excerpt, content, author, imageUrl' + }, { status: 400 }) + } + + const rawSlug = generateSlug(title) + const slug = `${rawSlug}-${Math.random().toString(36).substring(2, 7)}` + + // Si un "type" est fourni, on l'ajoute automatiquement aux tags pour le catégoriser + const finalTags = tags || [] + if (type && typeof type === 'string' && !finalTags.includes(type)) { + finalTags.push(type) + } + + const post = await prisma.blogPost.create({ + data: { + title, + excerpt, + content, + author, + imageUrl, + slug, + tags: finalTags, + status: status || 'PUBLISHED', + publishedAt: publishedAt ? new Date(publishedAt) : new Date(Date.now() - 60000), // -1 min to ensure visibility + date: new Date() + } + }) + + revalidatePath('/actualites') + revalidatePath('/') + + return NextResponse.json({ success: true, post }, { status: 201 }) + } catch (error) { + console.error('POST /api/external/articles error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/admin/src/app/api/external/events/route.ts b/admin/src/app/api/external/events/route.ts new file mode 100644 index 0000000..58b9f8c --- /dev/null +++ b/admin/src/app/api/external/events/route.ts @@ -0,0 +1,58 @@ +import { NextRequest, NextResponse } from 'next/server' +import prisma from '@/lib/prisma' +import { generateSlug } from '@/lib/utils' +import { revalidatePath } from 'next/cache' + +export async function POST(request: NextRequest) { + try { + // Simple API Key authentication + const apiKey = request.headers.get('x-api-key') + const expectedApiKey = process.env.EXTERNAL_API_KEY + + if (!expectedApiKey) { + console.error('EXTERNAL_API_KEY is not defined in environment variables.') + return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }) + } + + if (apiKey !== expectedApiKey) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + + // Validation + const { title, description, date, location, thumbnailUrl, link, tags, status, publishedAt } = body + + if (!title || !description || !date || !location || !thumbnailUrl) { + return NextResponse.json({ + error: 'Missing required fields: title, description, date, location, thumbnailUrl' + }, { status: 400 }) + } + + const rawSlug = generateSlug(title) + const slug = `${rawSlug}-${Math.random().toString(36).substring(2, 7)}` + + const event = await prisma.event.create({ + data: { + title, + description, + date: new Date(date), + location, + thumbnailUrl, + link, + slug, + tags: tags || [], + status: status || 'PUBLISHED', + publishedAt: publishedAt ? new Date(publishedAt) : new Date() + } + }) + + revalidatePath('/evenements') + revalidatePath('/') + + return NextResponse.json({ success: true, event }, { status: 201 }) + } catch (error) { + console.error('POST /api/external/events error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/admin/src/components/ActualitesForm.tsx b/admin/src/components/ActualitesForm.tsx index 18166ae..9de7d5d 100644 --- a/admin/src/components/ActualitesForm.tsx +++ b/admin/src/components/ActualitesForm.tsx @@ -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(initialData || {}); + const [jsonString, setJsonString] = useState(''); + const [showJsonInput, setShowJsonInput] = useState(false); + const [jsonError, setJsonError] = useState(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) => { event.preventDefault(); const formData = new FormData(event.currentTarget); @@ -111,7 +161,63 @@ export default function BlogForm({ initialData }: Props) { -
+ {/* PREFILL JSON SECTION */} +
+ + + {showJsonInput && ( +
+

+ 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. +

+ {jsonError && ( +
+ {jsonError} +
+ )} +