feat: implement comprehensive CMS dashboard for managing news, events, interviews, and one-shot pricing plans
This commit is contained in:
251
components/OneShotPricingSection.tsx
Normal file
251
components/OneShotPricingSection.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user