feat: implement comprehensive CMS dashboard for managing news, events, interviews, and one-shot pricing plans
This commit is contained in:
183
admin/src/components/OneShotPricingSettings.tsx
Normal file
183
admin/src/components/OneShotPricingSettings.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useTransition } from 'react';
|
||||
import { Save, Loader2, Zap, Calendar, Newspaper, Layout } from 'lucide-react';
|
||||
import { updateOneShotPlan } from '@/app/actions/one-shot';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { OneShotType } from '@prisma/client';
|
||||
|
||||
interface OneShotPlan {
|
||||
id: string;
|
||||
type: OneShotType;
|
||||
name: string;
|
||||
priceXOF: number;
|
||||
priceEUR: number;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
const TYPE_ICONS = {
|
||||
BUMP: Zap,
|
||||
POST_EVENT: Calendar,
|
||||
POST_NEWS: Newspaper,
|
||||
AD_DIRECTORY: Layout,
|
||||
};
|
||||
|
||||
const TYPE_LABELS = {
|
||||
BUMP: "Remonter dans l'annuaire",
|
||||
POST_EVENT: "Poster un événement",
|
||||
POST_NEWS: "Poster une actualité",
|
||||
AD_DIRECTORY: "Publicité dans l'annuaire",
|
||||
};
|
||||
|
||||
export default function OneShotPricingSettings({ initialPlans }: { initialPlans: OneShotPlan[] }) {
|
||||
const [plans, setPlans] = useState<OneShotPlan[]>(initialPlans);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
// Ensure all types are present even if not in DB yet
|
||||
const allTypes: OneShotType[] = ['BUMP', 'POST_EVENT', 'POST_NEWS', 'AD_DIRECTORY'];
|
||||
|
||||
const getPlanByType = (type: OneShotType) => {
|
||||
return plans.find(p => p.type === type) || {
|
||||
id: '',
|
||||
type,
|
||||
name: TYPE_LABELS[type],
|
||||
priceXOF: 0,
|
||||
priceEUR: 0,
|
||||
description: ''
|
||||
};
|
||||
};
|
||||
|
||||
const handleUpdate = async (type: OneShotType, data: any) => {
|
||||
startTransition(async () => {
|
||||
const result = await updateOneShotPlan(type, data);
|
||||
if (result.success) {
|
||||
toast.success(`Plan ${TYPE_LABELS[type]} mis à jour`);
|
||||
} else {
|
||||
toast.error(result.error || "Erreur lors de la mise à jour");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
|
||||
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-5 h-5 text-yellow-400" />
|
||||
<h2 className="font-semibold text-white">Tarifs des Services Ponctuels (One-Shot)</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (confirm("Initialiser tous les plans avec les valeurs par défaut ?")) {
|
||||
startTransition(async () => {
|
||||
for (const type of allTypes) {
|
||||
await updateOneShotPlan(type, {
|
||||
name: TYPE_LABELS[type],
|
||||
priceXOF: 5000,
|
||||
priceEUR: 8,
|
||||
description: `Service de ${TYPE_LABELS[type].toLowerCase()}`
|
||||
});
|
||||
}
|
||||
toast.success("Plans initialisés. Rechargement...");
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="text-xs bg-slate-800 hover:bg-slate-700 text-slate-300 px-3 py-1 rounded-lg border border-slate-700 transition-all"
|
||||
>
|
||||
Initialiser par défaut
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-8">
|
||||
<p className="text-sm text-slate-400">
|
||||
Configurez les prix des services à l'acte. Ces tarifs seront affichés sur la page de tarification.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{allTypes.map((type) => {
|
||||
const plan = getPlanByType(type);
|
||||
const Icon = TYPE_ICONS[type];
|
||||
|
||||
return (
|
||||
<div key={type} className="bg-slate-950/50 border border-slate-800 rounded-xl p-6 hover:border-slate-700 transition-colors">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center">
|
||||
<Icon className="w-6 h-6 text-indigo-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-white text-lg">{TYPE_LABELS[type]}</h3>
|
||||
<p className="text-xs text-slate-500 font-mono">{type}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
handleUpdate(type, {
|
||||
name: formData.get('name'),
|
||||
priceXOF: parseInt(formData.get('priceXOF') as string || '0'),
|
||||
priceEUR: parseInt(formData.get('priceEUR') as string || '0'),
|
||||
description: formData.get('description'),
|
||||
});
|
||||
}}
|
||||
className="flex-1 grid grid-cols-1 md:grid-cols-4 gap-4"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Nom affiché</label>
|
||||
<input
|
||||
name="name"
|
||||
defaultValue={plan.name}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-white focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Prix (XOF)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="priceXOF"
|
||||
defaultValue={plan.priceXOF}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-white focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Prix (EUR)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="priceEUR"
|
||||
defaultValue={plan.priceEUR}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-white focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-slate-800 text-white p-2 rounded-lg font-bold text-sm transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
<div className="md:col-span-4 space-y-1">
|
||||
<label className="text-[10px] font-bold text-slate-500 uppercase">Description courte</label>
|
||||
<input
|
||||
name="description"
|
||||
defaultValue={plan.description || ''}
|
||||
placeholder="Ex: Remontez votre entreprise en tête de liste pour 7 jours"
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-white focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user