feat: implement comprehensive CMS dashboard for managing news, events, interviews, and one-shot pricing plans

This commit is contained in:
2026-05-10 17:50:56 +02:00
parent 87b13dce13
commit 9846efd03e
40 changed files with 1487 additions and 175 deletions

View File

@@ -67,7 +67,7 @@ const Footer = ({ settings }: FooterProps) => {
<ul className="space-y-2 text-gray-400 text-sm">
<li><Link href="/annuaire" className="hover:text-brand-500">Rechercher une entreprise</Link></li>
<li><Link href="/login" className="hover:text-brand-500">Inscrire mon entreprise</Link></li>
<li><Link href="/blog" className="hover:text-brand-500">Actualités & Conseils</Link></li>
<li><Link href="/actualites" className="hover:text-brand-500">Actualités & Conseils</Link></li>
<li><Link href="/subscription" className="hover:text-brand-500">Tarifs</Link></li>
</ul>
</div>

View File

@@ -84,7 +84,7 @@ const HomeSlider = ({ slides, onSearch }: Props) => {
{/* Background Image */}
<div className="absolute inset-0">
<img
src={slide.type === 'BUSINESS' ? (slide.business?.coverUrl || slide.business?.logoUrl) : (slide.imageUrl || '')}
src={slide.imageUrl || slide.business?.coverUrl || slide.business?.logoUrl || ''}
className={`w-full h-full object-cover transition-transform duration-[6000ms] ease-linear ${
index === current ? 'scale-110' : 'scale-100'
}`}

View File

@@ -42,8 +42,8 @@ const Navbar = ({ settings }: NavbarProps) => {
<Link href="/afrolife" className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${pathname.startsWith('/afrolife') ? (isDev ? 'border-white text-white' : 'border-brand-500 text-brand-600') : (isDev ? 'border-transparent text-red-100 hover:text-white hover:border-red-200' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300')}`}>
Afro Life
</Link>
<Link href="/blog" className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${pathname.startsWith('/blog') ? (isDev ? 'border-white text-white' : 'border-brand-500 text-gray-900') : (isDev ? 'border-transparent text-red-100 hover:text-white hover:border-red-200' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300')}`}>
Blog
<Link href="/actualites" className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${pathname.startsWith('/actualites') ? (isDev ? 'border-white text-white' : 'border-brand-500 text-gray-900') : (isDev ? 'border-transparent text-red-100 hover:text-white hover:border-red-200' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300')}`}>
Actualités
</Link>
</div>
</div>
@@ -77,7 +77,7 @@ const Navbar = ({ settings }: NavbarProps) => {
<Link href="/" className="block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300" onClick={() => setIsOpen(false)}>Accueil</Link>
<Link href="/annuaire" className="block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300" onClick={() => setIsOpen(false)}>Annuaire</Link>
<Link href="/afrolife" className="block pl-3 pr-4 py-2 border-l-4 border-brand-500 text-base font-medium text-brand-700 bg-brand-50" onClick={() => setIsOpen(false)}>Afro Life</Link>
<Link href="/blog" className="block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300" onClick={() => setIsOpen(false)}>Blog</Link>
<Link href="/actualites" className="block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300" onClick={() => setIsOpen(false)}>Actualités</Link>
{user ? (
<Link href="/dashboard" className="block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300" onClick={() => setIsOpen(false)}>Mon Espace</Link>
) : (

View File

@@ -0,0 +1,251 @@
"use client";
import React, { useState, useEffect } from 'react';
import { Zap, Calendar, Newspaper, Layout, Check, Smartphone, CreditCard, Loader, ShieldCheck, X } from 'lucide-react';
import { useUser } from './UserProvider';
import { useRouter } from 'next/navigation';
import CreateEventModal from './dashboard/CreateEventModal';
import CreateNewsModal from './dashboard/CreateNewsModal';
import { toast } from 'react-hot-toast';
interface OneShotPlan {
id: string;
type: string;
name: string;
priceXOF: number;
priceEUR: number;
description: string | null;
}
const TYPE_ICONS = {
BUMP: Zap,
POST_EVENT: Calendar,
POST_NEWS: Newspaper,
AD_DIRECTORY: Layout,
};
const OneShotPricingSection = () => {
const [plans, setPlans] = useState<OneShotPlan[]>([]);
const [loading, setLoading] = useState(true);
const [selectedPlan, setSelectedPlan] = useState<OneShotPlan | null>(null);
const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
const [paymentStep, setPaymentStep] = useState<'method' | 'processing' | 'success'>('method');
const [paymentMethod, setPaymentMethod] = useState<'mobile_money' | 'card' | null>(null);
const [showEventModal, setShowEventModal] = useState(false);
const [showNewsModal, setShowNewsModal] = useState(false);
const { user } = useUser();
const router = useRouter();
useEffect(() => {
const fetchPlans = async () => {
try {
const res = await fetch('/api/one-shot-plans');
const data = await res.json();
if (!data.error) {
setPlans(data);
}
} catch (error) {
console.error('Error fetching one-shot plans:', error);
} finally {
setLoading(false);
}
};
fetchPlans();
}, []);
const handleSelectPlan = (plan: OneShotPlan) => {
if (!user) {
toast.error("Veuillez vous connecter pour commander un service");
router.push('/login?callback=/subscription');
return;
}
setSelectedPlan(plan);
setPaymentStep('method');
setIsPaymentModalOpen(true);
};
const handlePayment = () => {
setPaymentStep('processing');
setTimeout(() => {
setPaymentStep('success');
}, 2500);
};
const closePayment = () => {
setIsPaymentModalOpen(false);
setPaymentStep('method');
setSelectedPlan(null);
setPaymentMethod(null);
};
if (!loading && plans.length === 0) return null;
return (
<div className="py-16 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-base font-semibold text-brand-600 tracking-wide uppercase">Services à la carte</h2>
<p className="mt-1 text-3xl font-extrabold text-gray-900 sm:text-4xl font-serif">
Boostez votre visibilité ponctuellement
</p>
<p className="max-w-xl mt-4 mx-auto text-lg text-gray-500">
Des options flexibles pour mettre en avant vos actualités et vos offres sans engagement.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{loading ? (
[1, 2, 3, 4].map((i) => (
<div key={i} className="bg-gray-50 rounded-2xl p-6 h-64 animate-pulse border border-gray-100">
<div className="w-12 h-12 bg-gray-200 rounded-xl mb-4"></div>
<div className="h-6 bg-gray-200 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-full mb-8"></div>
<div className="h-10 bg-gray-200 rounded w-full"></div>
</div>
))
) : (
plans.map((plan) => {
const Icon = TYPE_ICONS[plan.type as keyof typeof TYPE_ICONS] || Zap;
return (
<div key={plan.id} className="bg-gray-50 rounded-2xl p-8 border border-gray-100 hover:border-brand-200 hover:shadow-lg transition-all group flex flex-col">
<div className="w-14 h-14 bg-brand-50 rounded-2xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
<Icon className="w-8 h-8 text-brand-600" />
</div>
<h3 className="text-xl font-bold text-gray-900 font-serif mb-2">{plan.name}</h3>
<p className="text-sm text-gray-500 mb-6 flex-1">{plan.description}</p>
<div className="mb-6">
<span className="text-3xl font-black text-gray-900">{plan.priceXOF.toLocaleString()} F CFA</span>
<p className="text-xs text-gray-400 mt-1">env. {plan.priceEUR} </p>
</div>
<button
onClick={() => handleSelectPlan(plan)}
className="w-full bg-white border-2 border-brand-600 text-brand-600 hover:bg-brand-600 hover:text-white font-bold py-3 rounded-xl transition-all shadow-sm"
>
Commander
</button>
</div>
);
})
)}
</div>
</div>
{/* PAYMENT MODAL (Simplified copy from PricingSection) */}
{isPaymentModalOpen && selectedPlan && (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen px-4">
<div className="fixed inset-0 bg-gray-900/60 backdrop-blur-sm transition-opacity" onClick={closePayment}></div>
<div className="relative bg-white rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all max-w-lg w-full">
<div className="px-6 py-8">
<div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-bold text-gray-900 font-serif">Paiement Service Ponctuel</h3>
<button onClick={closePayment} className="text-gray-400 hover:text-gray-600"><X className="h-6 w-6" /></button>
</div>
{paymentStep === 'method' && (
<>
<div className="bg-brand-50 p-5 rounded-2xl mb-8 border border-brand-100">
<p className="text-sm text-brand-800 font-medium">Option : <span className="font-bold">{selectedPlan.name}</span></p>
<p className="text-3xl font-black text-brand-900 mt-1">{selectedPlan.priceXOF.toLocaleString()} F CFA</p>
</div>
<div className="space-y-3">
<button
onClick={() => setPaymentMethod('mobile_money')}
className={`w-full flex items-center p-4 border-2 rounded-xl transition-all ${paymentMethod === 'mobile_money' ? 'border-brand-500 bg-brand-50/50' : 'border-gray-100 hover:bg-gray-50'}`}
>
<div className="h-10 w-10 bg-orange-100 rounded-xl flex items-center justify-center text-orange-600 mr-4"><Smartphone /></div>
<div className="text-left flex-1">
<p className="font-bold text-gray-900">Mobile Money</p>
<p className="text-xs text-gray-500">Orange, MTN, Wave, Moov</p>
</div>
</button>
<button
onClick={() => setPaymentMethod('card')}
className={`w-full flex items-center p-4 border-2 rounded-xl transition-all ${paymentMethod === 'card' ? 'border-brand-500 bg-brand-50/50' : 'border-gray-100 hover:bg-gray-50'}`}
>
<div className="h-10 w-10 bg-blue-100 rounded-xl flex items-center justify-center text-blue-600 mr-4"><CreditCard /></div>
<div className="text-left flex-1">
<p className="font-bold text-gray-900">Carte Bancaire</p>
<p className="text-xs text-gray-500">Visa, Mastercard</p>
</div>
</button>
</div>
<button
disabled={!paymentMethod}
onClick={handlePayment}
className="w-full mt-8 bg-brand-600 text-white font-bold py-4 rounded-xl shadow-lg hover:bg-brand-700 disabled:opacity-50 transition-all"
>
Payer maintenant
</button>
</>
)}
{paymentStep === 'processing' && (
<div className="py-12 text-center">
<Loader className="h-14 w-14 text-brand-600 animate-spin mx-auto mb-6" />
<p className="text-xl font-bold text-gray-900">Sécurisation du paiement...</p>
<p className="text-gray-500 mt-2">N'interrompez pas la transaction.</p>
</div>
)}
{paymentStep === 'success' && (
<div className="py-8 text-center animate-in zoom-in duration-300">
<div className="mx-auto flex items-center justify-center h-20 w-20 rounded-full bg-green-100 mb-6">
<ShieldCheck className="h-12 w-12 text-green-600" />
</div>
<h3 className="text-2xl font-bold text-gray-900">Paiement Réussi !</h3>
<p className="text-gray-500 mt-3 mb-8">
{selectedPlan.type === 'POST_EVENT' || selectedPlan.type === 'POST_NEWS'
? "Votre paiement a été validé. Vous pouvez maintenant remplir le formulaire de votre publication."
: `Votre service ${selectedPlan.name} a été activé avec succès.`
}
</p>
{selectedPlan.type === 'POST_EVENT' || selectedPlan.type === 'POST_NEWS' ? (
<button
onClick={() => {
setIsPaymentModalOpen(false);
if (selectedPlan.type === 'POST_EVENT') setShowEventModal(true);
if (selectedPlan.type === 'POST_NEWS') setShowNewsModal(true);
}}
className="w-full bg-brand-600 text-white font-bold py-4 rounded-xl shadow-lg hover:bg-brand-700 transition-all flex items-center justify-center gap-2"
>
{selectedPlan.type === 'POST_EVENT' ? <Calendar className="w-5 h-5" /> : <Newspaper className="w-5 h-5" />}
Remplir le formulaire
</button>
) : (
<button onClick={closePayment} className="w-full bg-brand-600 text-white font-bold py-4 rounded-xl shadow-lg hover:bg-brand-700 transition-all">Retourner au site</button>
)}
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* EVENT FORM MODAL */}
<CreateEventModal
isOpen={showEventModal}
onClose={() => {
setShowEventModal(false);
closePayment();
}}
/>
{/* NEWS FORM MODAL */}
<CreateNewsModal
isOpen={showNewsModal}
onClose={() => {
setShowNewsModal(false);
closePayment();
}}
/>
</div>
);
};
export default OneShotPricingSection;

View File

@@ -0,0 +1,229 @@
"use client";
import React, { useState, useTransition } from 'react';
import { X, Calendar, MapPin, Link as LinkIcon, Image as ImageIcon, AlertTriangle, Loader2 } from 'lucide-react';
import { createEvent } from '@/app/actions/events';
import { uploadImage } from '@/app/actions/upload';
import { toast } from 'react-hot-toast';
interface CreateEventModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function CreateEventModal({ isOpen, onClose }: CreateEventModalProps) {
const [isPending, startTransition] = useTransition();
const [imagePreview, setImagePreview] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
if (!isOpen) return null;
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = {
title: formData.get('title') as string,
description: formData.get('description') as string,
date: formData.get('date') as string,
location: formData.get('location') as string,
thumbnailUrl: imagePreview || "https://picsum.photos/800/400?random=event",
link: formData.get('link') as string || undefined,
};
if (!data.title || !data.description || !data.date || !data.location) {
toast.error("Veuillez remplir tous les champs obligatoires");
return;
}
startTransition(async () => {
const result = await createEvent(data);
if (result.success) {
toast.success("Événement soumis avec succès !");
onClose();
} else {
toast.error(result.error || "Une erreur est survenue");
}
});
};
const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append('file', file);
const result = await uploadImage(formData);
if (result.success && result.url) {
setImagePreview(result.url);
} else {
toast.error(result.error || "Erreur lors de l'upload");
}
setUploading(false);
};
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-2xl overflow-hidden animate-in zoom-in-95 duration-300">
<div className="p-6 border-b border-gray-100 flex items-center justify-between bg-brand-50/50">
<div className="flex items-center gap-3">
<div className="p-2 bg-brand-100 rounded-xl text-brand-600">
<Calendar className="w-6 h-6" />
</div>
<div>
<h2 className="text-xl font-bold text-gray-900">Publier un événement</h2>
<p className="text-xs text-gray-500">Partagez votre actualité avec la communauté</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<X className="w-6 h-6 text-gray-400" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6 max-h-[80vh] overflow-y-auto">
{/* Warning */}
<div className="flex gap-4 p-4 bg-amber-50 border border-amber-200 rounded-2xl">
<div className="shrink-0 p-2 bg-amber-100 rounded-xl text-amber-600">
<AlertTriangle className="w-5 h-5" />
</div>
<div>
<h4 className="text-sm font-bold text-amber-800">Information importante</h4>
<p className="text-xs text-amber-700 leading-relaxed">
Votre événement sera soumis à la validation de notre équipe de modération avant d'être publié sur la plateforme (généralement sous 24h).
</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Titre de l'événement *</label>
<input
name="title"
required
placeholder="Ex: Conférence sur l'Agrotech"
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-brand-500 outline-none transition-all"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Date *</label>
<div className="relative">
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
name="date"
type="date"
required
className="w-full pl-11 pr-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-brand-500 outline-none transition-all"
/>
</div>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Lieu *</label>
<div className="relative">
<MapPin className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
name="location"
required
placeholder="Ex: Abidjan, Côte d'Ivoire"
className="w-full pl-11 pr-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-brand-500 outline-none transition-all"
/>
</div>
</div>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Description *</label>
<textarea
name="description"
required
rows={4}
placeholder="Présentez votre événement en quelques lignes..."
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-brand-500 outline-none transition-all resize-none"
></textarea>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Affiche / Photo de l'événement</label>
<div
className={`relative border-2 border-dashed rounded-2xl flex flex-col items-center justify-center p-8 transition-all ${
imagePreview ? 'border-brand-500 bg-brand-50' : 'border-gray-200 hover:border-brand-300 bg-gray-50'
}`}
>
{imagePreview ? (
<div className="relative w-full aspect-video rounded-lg overflow-hidden shadow-inner">
<img src={imagePreview} alt="Preview" className="w-full h-full object-cover" />
<button
type="button"
onClick={() => setImagePreview(null)}
className="absolute top-2 right-2 p-1 bg-white/80 rounded-full hover:bg-white text-gray-600 shadow-lg"
>
<X className="w-4 h-4" />
</button>
</div>
) : (
<>
{uploading ? (
<Loader2 className="w-10 h-10 text-brand-500 animate-spin" />
) : (
<ImageIcon className="w-10 h-10 text-gray-300 mb-2" />
)}
<p className="text-sm font-medium text-gray-500">Cliquez pour ajouter une image</p>
<p className="text-xs text-gray-400 mt-1">PNG, JPG jusqu'à 2Mo</p>
<input
type="file"
accept="image/*"
onChange={handleImageChange}
className="absolute inset-0 opacity-0 cursor-pointer"
/>
</>
)}
</div>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Lien externe (facultatif)</label>
<div className="relative">
<LinkIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
name="link"
placeholder="https://votre-billetterie.com"
className="w-full pl-11 pr-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-brand-500 outline-none transition-all"
/>
</div>
</div>
</div>
<div className="flex items-center gap-4 pt-4">
<button
type="button"
onClick={onClose}
className="flex-1 px-6 py-4 rounded-2xl font-bold text-gray-600 hover:bg-gray-100 transition-all"
>
Annuler
</button>
<button
type="submit"
disabled={isPending || uploading}
className="flex-[2] bg-brand-600 hover:bg-brand-700 disabled:bg-gray-200 disabled:cursor-not-allowed text-white px-6 py-4 rounded-2xl font-bold shadow-xl shadow-brand-200 transition-all flex items-center justify-center gap-2"
>
{isPending ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Soumission...
</>
) : (
<>
<Calendar className="w-5 h-5" />
Soumettre l'événement
</>
)}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,216 @@
"use client";
import React, { useState, useTransition } from 'react';
import { X, Newspaper, User, Image as ImageIcon, AlertTriangle, Loader2, AlignLeft } from 'lucide-react';
import { createNews } from '@/app/actions/news';
import { uploadImage } from '@/app/actions/upload';
import { toast } from 'react-hot-toast';
interface CreateNewsModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function CreateNewsModal({ isOpen, onClose }: CreateNewsModalProps) {
const [isPending, startTransition] = useTransition();
const [imagePreview, setImagePreview] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
if (!isOpen) return null;
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = {
title: formData.get('title') as string,
excerpt: formData.get('excerpt') as string,
content: formData.get('content') as string,
author: formData.get('author') as string,
imageUrl: imagePreview || "https://picsum.photos/800/400?random=news",
};
if (!data.title || !data.excerpt || !data.content || !data.author) {
toast.error("Veuillez remplir tous les champs obligatoires");
return;
}
startTransition(async () => {
const result = await createNews(data);
if (result.success) {
toast.success("Actualité soumise avec succès !");
onClose();
} else {
toast.error(result.error || "Une erreur est survenue");
}
});
};
const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append('file', file);
const result = await uploadImage(formData);
if (result.success && result.url) {
setImagePreview(result.url);
} else {
toast.error(result.error || "Erreur lors de l'upload");
}
setUploading(false);
};
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-2xl overflow-hidden animate-in zoom-in-95 duration-300">
<div className="p-6 border-b border-gray-100 flex items-center justify-between bg-indigo-50/50">
<div className="flex items-center gap-3">
<div className="p-2 bg-indigo-100 rounded-xl text-indigo-600">
<Newspaper className="w-6 h-6" />
</div>
<div>
<h2 className="text-xl font-bold text-gray-900">Publier une actualité</h2>
<p className="text-xs text-gray-500">Mettez en avant vos succès et nouveautés</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<X className="w-6 h-6 text-gray-400" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6 max-h-[80vh] overflow-y-auto">
{/* Warning */}
<div className="flex gap-4 p-4 bg-amber-50 border border-amber-200 rounded-2xl">
<div className="shrink-0 p-2 bg-amber-100 rounded-xl text-amber-600">
<AlertTriangle className="w-5 h-5" />
</div>
<div>
<h4 className="text-sm font-bold text-amber-800">Information importante</h4>
<p className="text-xs text-amber-700 leading-relaxed">
Votre article sera soumis à la validation de notre équipe de modération avant d'être publié dans la section Actualités.
</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Titre de l'article *</label>
<input
name="title"
required
placeholder="Ex: Lancement de notre nouvelle gamme de produits"
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-indigo-500 outline-none transition-all"
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Auteur *</label>
<div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
name="author"
required
placeholder="Votre nom ou nom d'entreprise"
className="w-full pl-11 pr-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-indigo-500 outline-none transition-all"
/>
</div>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Résumé court (Excerpt) *</label>
<div className="relative">
<AlignLeft className="absolute left-4 top-4 w-4 h-4 text-gray-400" />
<textarea
name="excerpt"
required
rows={2}
placeholder="Un court résumé qui apparaîtra dans la liste des articles..."
className="w-full pl-11 pr-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-indigo-500 outline-none transition-all resize-none"
></textarea>
</div>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Contenu de l'article *</label>
<textarea
name="content"
required
rows={6}
placeholder="Racontez votre histoire..."
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-indigo-500 outline-none transition-all resize-none"
></textarea>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Image de couverture</label>
<div
className={`relative border-2 border-dashed rounded-2xl flex flex-col items-center justify-center p-8 transition-all ${
imagePreview ? 'border-indigo-500 bg-indigo-50' : 'border-gray-200 hover:border-indigo-300 bg-gray-50'
}`}
>
{imagePreview ? (
<div className="relative w-full aspect-video rounded-lg overflow-hidden shadow-inner">
<img src={imagePreview} alt="Preview" className="w-full h-full object-cover" />
<button
type="button"
onClick={() => setImagePreview(null)}
className="absolute top-2 right-2 p-1 bg-white/80 rounded-full hover:bg-white text-gray-600 shadow-lg"
>
<X className="w-4 h-4" />
</button>
</div>
) : (
<>
{uploading ? (
<Loader2 className="w-10 h-10 text-indigo-500 animate-spin" />
) : (
<ImageIcon className="w-10 h-10 text-gray-300 mb-2" />
)}
<p className="text-sm font-medium text-gray-500">Cliquez pour ajouter une image</p>
<p className="text-xs text-gray-400 mt-1">PNG, JPG jusqu'à 2Mo</p>
<input
type="file"
accept="image/*"
onChange={handleImageChange}
className="absolute inset-0 opacity-0 cursor-pointer"
/>
</>
)}
</div>
</div>
</div>
<div className="flex items-center gap-4 pt-4">
<button
type="button"
onClick={onClose}
className="flex-1 px-6 py-4 rounded-2xl font-bold text-gray-600 hover:bg-gray-100 transition-all"
>
Annuler
</button>
<button
type="submit"
disabled={isPending || uploading}
className="flex-[2] bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-200 disabled:cursor-not-allowed text-white px-6 py-4 rounded-2xl font-bold shadow-xl shadow-indigo-200 transition-all flex items-center justify-center gap-2"
>
{isPending ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Soumission...
</>
) : (
<>
<Newspaper className="w-5 h-5" />
Soumettre l'actualité
</>
)}
</button>
</div>
</form>
</div>
</div>
);
}