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

@@ -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;