feat: implement core platform features including business directory, admin dashboard, authentication, and API infrastructure
Some checks failed
Build and Push App / build (push) Failing after 16m2s

This commit is contained in:
2026-04-18 22:10:19 +02:00
parent 6ec1a3ae84
commit 3e2063e4fa
89 changed files with 4405 additions and 967 deletions

View File

@@ -31,12 +31,24 @@ export default function AnalyticsTracker() {
// 1. Track Page Views
useEffect(() => {
// Attempt to get userId from localStorage (common in this app's auth setup)
let userId = null;
try {
const userStr = localStorage.getItem('user');
if (userStr) {
const user = JSON.parse(userStr);
userId = user.id;
}
} catch (e) {}
sendEvent({
type: 'PAGE_VIEW',
path: pathname,
metadata: {
userAgent: navigator.userAgent,
screen: `${window.innerWidth}x${window.innerHeight}`,
referrer: document.referrer,
userId: userId
}
});

View File

@@ -18,7 +18,7 @@ const BusinessCard: React.FC<{ business: Business }> = ({ business }) => {
const handleShare = (platform: string) => {
const url = encodeURIComponent(`${window.location.origin}/annuaire/${business.id}`);
const text = encodeURIComponent(`Découvrez ${business.name} sur Afropreunariat`);
const text = encodeURIComponent(`Découvrez ${business.name} sur Afrohub`);
let shareLink = '';
switch(platform) {

View File

@@ -37,7 +37,7 @@ const CookieBanner = () => {
<div>
<h3 className="text-lg font-bold mb-1">Nous respectons votre vie privée</h3>
<p className="text-sm text-gray-400 leading-relaxed">
Afropreunariat utilise des cookies pour améliorer votre expérience, analyser le trafic et personnaliser le contenu.
Afrohub utilise des cookies pour améliorer votre expérience, analyser le trafic et personnaliser le contenu.
Certains sont essentiels, d'autres nous aident à grandir avec vous.
Consultez nos <Link href="/cgu" className="text-brand-500 hover:underline">CGU</Link> pour en savoir plus.
</p>

View File

@@ -4,6 +4,7 @@
import React from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Facebook, Twitter, Instagram, Linkedin } from 'lucide-react';
interface FooterProps {
settings?: any;
@@ -14,23 +15,52 @@ const Footer = ({ settings }: FooterProps) => {
if (pathname.startsWith('/dashboard')) return null;
const siteInfo = settings || {
siteName: "Afropreunariat",
siteName: "Afrohub",
siteSlogan: "La plateforme de référence pour l'entrepreneuriat africain. Visibilité, connexion et croissance pour les TPE/PME.",
contactEmail: "support@afropreunariat.com",
contactPhone: "+225 00 00 00 00 00",
contactEmail: "support@afrohub.com",
address: "Abidjan, Côte d'Ivoire / Paris, France",
footerText: "© 2025 Afropreunariat. Tous droits réservés."
footerText: "© 2025 Afrohub. Tous droits réservés.",
facebookUrl: "#",
twitterUrl: "#",
instagramUrl: "#",
linkedinUrl: "#"
};
const socialLinks = [
{ icon: Facebook, url: siteInfo.facebookUrl, label: "Facebook" },
{ icon: Twitter, url: siteInfo.twitterUrl, label: "Twitter" },
{ icon: Instagram, url: siteInfo.instagramUrl, label: "Instagram" },
{ icon: Linkedin, url: siteInfo.linkedinUrl, label: "LinkedIn" },
].filter(link => link.url && link.url.trim() !== "" && link.url !== "#");
return (
<footer className="bg-dark-900 text-white">
<div className="max-w-7xl mx-auto py-12 px-4 overflow-hidden sm:px-6 lg:px-8">
<footer className="bg-dark-900 text-white border-t border-gray-800">
<div className="max-w-7xl mx-auto py-12 px-4 overflow-hidden sm:px-6 lg:px-8 text-center sm:text-left">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
<div>
<h3 className="text-2xl font-serif font-bold text-brand-500 mb-4">{siteInfo.siteName}</h3>
<p className="text-gray-400 text-sm">
<h3 className="text-2xl font-serif font-bold text-brand-500 mb-4 tracking-tight uppercase">{siteInfo.siteName}</h3>
<p className="text-gray-400 text-sm mb-6 max-w-sm">
{siteInfo.siteSlogan}
</p>
{socialLinks.length > 0 && (
<div className="space-y-4">
<h4 className="font-bold text-xs uppercase tracking-widest text-gray-500">Suivez-nous</h4>
<div className="flex space-x-4 justify-center sm:justify-start">
{socialLinks.map((social, index) => (
<a
key={index}
href={social.url}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-full border border-gray-800 text-gray-400 hover:text-brand-500 hover:border-brand-500/50 hover:bg-white/5 transition-all"
aria-label={social.label}
>
<social.icon size={18} />
</a>
))}
</div>
</div>
)}
</div>
<div>
<h4 className="font-bold text-lg mb-4">Liens Rapides</h4>

View File

@@ -21,7 +21,7 @@ const Navbar = () => {
<div className="flex items-center">
<Link href="/" className="flex-shrink-0 flex items-center gap-2">
<div className="w-8 h-8 bg-brand-600 rounded-lg flex items-center justify-center text-white font-bold font-serif">A</div>
<span className="font-serif font-bold text-xl text-gray-900">Afropreunariat</span>
<span className="font-serif font-bold text-xl text-gray-900">Afrohub</span>
</Link>
<div className="hidden sm:ml-8 sm:flex sm:space-x-8">
<Link href="/" className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${pathname === '/' ? 'border-brand-500 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}`}>

View File

@@ -1,80 +1,52 @@
"use client";
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Check, X, Smartphone, CreditCard, Loader, ShieldCheck } from 'lucide-react';
interface Plan {
id: string;
tier: string;
name: string;
priceXOF: string;
priceEUR: string;
description: string;
features: string[];
recommended?: boolean;
recommended: boolean;
color: string;
offerLimit: number;
}
const PLANS: Plan[] = [
{
id: 'starter',
name: 'Starter',
priceXOF: 'Gratuit',
priceEUR: '0€',
description: 'Pour démarrer votre présence en ligne.',
features: [
'Fiche entreprise basique',
'Visible dans la recherche',
'1 Offre produit/service',
'Support par email'
],
color: 'gray'
},
{
id: 'booster',
name: 'Booster',
priceXOF: '5.000 FCFA',
priceEUR: '8€',
description: 'L\'indispensable pour les entreprises en croissance.',
recommended: true,
features: [
'Tout du plan Starter',
'Badge "Vérifié" ✅',
'Jusqu\'à 10 Offres produits',
'Lien vers réseaux sociaux & Site Web',
'Statistiques de base (Vues)'
],
color: 'brand'
},
{
id: 'empire',
name: 'Empire',
priceXOF: '15.000 FCFA',
priceEUR: '23€',
description: 'Dominez votre marché avec une visibilité maximale.',
features: [
'Tout du plan Booster',
'Badge "Recommandé" 🏆',
'Offres illimitées',
'Intégration vidéo Youtube',
'Interview écrite sur le Blog',
'Support prioritaire WhatsApp'
],
color: 'gray'
}
];
const PricingSection = () => {
const [plans, setPlans] = useState<Plan[]>([]);
const [loadingPlans, setLoadingPlans] = useState(true);
const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
useEffect(() => {
const fetchPlans = async () => {
try {
const res = await fetch('/api/plans');
const data = await res.json();
if (!data.error) {
setPlans(data);
}
} catch (error) {
console.error('Error fetching plans:', error);
} finally {
setLoadingPlans(false);
}
};
fetchPlans();
}, []);
// Payment State
const [paymentStep, setPaymentStep] = useState<'method' | 'processing' | 'success'>('method');
const [paymentMethod, setPaymentMethod] = useState<'mobile_money' | 'card' | null>(null);
const handleSelectPlan = (plan: Plan) => {
if (plan.id === 'starter') return; // Free plan, no payment
if (plan.tier === 'STARTER') return; // Free plan, no payment
setSelectedPlan(plan);
setPaymentStep('method');
setIsPaymentModalOpen(true);
@@ -130,53 +102,68 @@ const PricingSection = () => {
{/* Plans Grid */}
<div className="mt-12 space-y-4 sm:mt-16 sm:space-y-0 sm:grid sm:grid-cols-2 sm:gap-6 lg:max-w-4xl lg:mx-auto xl:max-w-none xl:mx-0 xl:grid-cols-3">
{PLANS.map((plan) => (
<div key={plan.id} className={`rounded-2xl shadow-xl bg-white border-2 flex flex-col ${plan.recommended ? 'border-brand-500 ring-4 ring-brand-50 relative transform scale-105 z-10' : 'border-gray-100'}`}>
{plan.recommended && (
<div className="absolute top-0 inset-x-0 -mt-4 flex justify-center">
<span className="bg-brand-500 text-white px-4 py-1 rounded-full text-xs font-bold uppercase tracking-wider shadow-sm">
Populaire
</span>
{loadingPlans ? (
[1, 2, 3].map((i) => (
<div key={i} className="rounded-2xl shadow-xl bg-white border border-gray-100 p-8 h-96 animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/2 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-3/4 mb-10"></div>
<div className="h-10 bg-gray-200 rounded mb-4"></div>
<div className="space-y-3 mt-10">
<div className="h-4 bg-gray-200 rounded w-full"></div>
<div className="h-4 bg-gray-200 rounded w-full"></div>
<div className="h-4 bg-gray-200 rounded w-full"></div>
</div>
)}
<div className="p-6 md:p-8 flex-1">
<h3 className="text-2xl font-bold text-gray-900 font-serif">{plan.name}</h3>
<p className="mt-4 text-sm text-gray-500">{plan.description}</p>
<div className="mt-8 flex items-baseline">
<span className="text-4xl font-extrabold text-gray-900 tracking-tight">
{plan.priceXOF === 'Gratuit' ? 'Gratuit' : (billingCycle === 'yearly' && plan.id !== 'starter' ? 'Sur Devis' : plan.priceXOF)}
</span>
{plan.priceXOF !== 'Gratuit' && <span className="ml-1 text-xl font-medium text-gray-500">/mois</span>}
</div>
{plan.priceXOF !== 'Gratuit' && (
<p className="text-xs text-gray-400 mt-1">soit env. {plan.priceEUR} /mois</p>
</div>
))
) : (
plans.map((plan) => (
<div key={plan.id} className={`rounded-2xl shadow-xl bg-white border-2 flex flex-col ${plan.recommended ? 'border-brand-500 ring-4 ring-brand-50 relative transform scale-105 z-10' : 'border-gray-100'}`}>
{plan.recommended && (
<div className="absolute top-0 inset-x-0 -mt-4 flex justify-center">
<span className="bg-brand-500 text-white px-4 py-1 rounded-full text-xs font-bold uppercase tracking-wider shadow-sm">
Populaire
</span>
</div>
)}
<div className="p-6 md:p-8 flex-1">
<h3 className="text-2xl font-bold text-gray-900 font-serif">{plan.name}</h3>
<p className="mt-4 text-sm text-gray-500">{plan.description}</p>
<div className="mt-8 flex items-baseline">
<span className="text-4xl font-extrabold text-gray-900 tracking-tight">
{plan.priceXOF === 'Gratuit' ? 'Gratuit' : (billingCycle === 'yearly' && plan.tier !== 'STARTER' ? 'Sur Devis' : plan.priceXOF)}
</span>
{plan.priceXOF !== 'Gratuit' && <span className="ml-1 text-xl font-medium text-gray-500">/mois</span>}
</div>
{plan.priceXOF !== 'Gratuit' && (
<p className="text-xs text-gray-400 mt-1">soit env. {plan.priceEUR} /mois</p>
)}
<ul className="mt-8 space-y-4">
{plan.features.map((feature) => (
<li key={feature} className="flex items-start">
<div className="flex-shrink-0">
<Check className="h-5 w-5 text-green-500" />
</div>
<p className="ml-3 text-sm text-gray-700">{feature}</p>
</li>
))}
</ul>
<ul className="mt-8 space-y-4">
{plan.features.map((feature) => (
<li key={feature} className="flex items-start">
<div className="flex-shrink-0">
<Check className="h-5 w-5 text-green-500" />
</div>
<p className="ml-3 text-sm text-gray-700">{feature}</p>
</li>
))}
</ul>
</div>
<div className="p-6 bg-gray-50 rounded-b-2xl">
<button
onClick={() => handleSelectPlan(plan)}
className={`w-full block text-center rounded-lg border border-transparent px-6 py-3 text-base font-medium transition-colors ${
plan.tier === 'STARTER'
? 'text-brand-700 bg-brand-100 hover:bg-brand-200'
: 'text-white bg-brand-600 hover:bg-brand-700 shadow-md hover:shadow-lg'
}`}
>
{plan.tier === 'STARTER' ? 'Commencer Gratuitement' : `Choisir ${plan.name}`}
</button>
</div>
</div>
<div className="p-6 bg-gray-50 rounded-b-2xl">
<button
onClick={() => handleSelectPlan(plan)}
className={`w-full block text-center rounded-lg border border-transparent px-6 py-3 text-base font-medium transition-colors ${
plan.id === 'starter'
? 'text-brand-700 bg-brand-100 hover:bg-brand-200'
: 'text-white bg-brand-600 hover:bg-brand-700 shadow-md hover:shadow-lg'
}`}
>
{plan.id === 'starter' ? 'Commencer Gratuitement' : `Choisir ${plan.name}`}
</button>
</div>
</div>
))}
))
)}
</div>
</div>

View File

@@ -1,15 +1,16 @@
"use client";
import React, { useState, useEffect } from 'react';
import { PlusCircle, Edit3, Trash2, ShoppingBag, Image as ImageIcon, Loader2 } from 'lucide-react';
import { PlusCircle, Edit3, Trash2, ShoppingBag, Image as ImageIcon, Loader2, AlertCircle } from 'lucide-react';
import { Offer, OfferType } from '../../types';
import { toast } from 'react-hot-toast';
interface DashboardOffersProps {
businessId: string;
plan?: string;
}
const DashboardOffers = ({ businessId }: DashboardOffersProps) => {
const DashboardOffers = ({ businessId, plan = 'STARTER' }: DashboardOffersProps) => {
const [offers, setOffers] = useState<Offer[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -155,19 +156,42 @@ const DashboardOffers = ({ businessId }: DashboardOffersProps) => {
);
}
const offerLimit = plan === 'BOOSTER' ? 10 : (plan === 'EMPIRE' ? 1000 : 1);
const isLimitReached = offers.length >= offerLimit;
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold font-serif text-gray-900">Mes Offres</h2>
<div className="flex flex-col">
<h2 className="text-2xl font-bold font-serif text-gray-900">Mes Offres</h2>
<p className="text-xs text-gray-500 mt-1">
Utilisation : <span className="font-bold">{offers.length} / {offerLimit === 1000 ? 'Illimité' : offerLimit}</span>
</p>
</div>
<button
onClick={openAddModal}
className="flex items-center bg-brand-600 text-white px-4 py-2 rounded-md hover:bg-brand-700 font-medium text-sm shadow-sm transition-colors active:scale-95"
disabled={isLimitReached}
className={`flex items-center px-4 py-2 rounded-md font-medium text-sm shadow-sm transition-colors active:scale-95 ${isLimitReached ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-brand-600 text-white hover:bg-brand-700'}`}
>
<PlusCircle className="w-4 h-4 mr-2" />
Ajouter une offre
</button>
</div>
{isLimitReached && (
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-orange-600" />
<p className="text-sm text-orange-800">
Vous avez atteint la limite de votre plan <strong>{plan}</strong>.
</p>
</div>
<button className="text-xs font-bold text-brand-600 hover:text-brand-700 uppercase tracking-tight">
Passer au plan supérieur →
</button>
</div>
)}
{/* List View */}
{offers.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">

View File

@@ -3,61 +3,100 @@ import React from 'react';
import { Eye, Star } from 'lucide-react';
import { Business } from '../../types';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
BarChart,
Bar
} from 'recharts';
const MousePointerClick = ({className}: {className?:string}) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M14 4.1 12 6" /><path d="m5.1 8-2.9-.8" /><path d="m6 12-1.9 2" /><path d="M7.2 2.2 8 5.1" /><path d="M9.037 9.69a.498.498 0 0 1 .653-.653l11 4.5a.5.5 0 0 1-.074.949l-4.349 1.041a1 1 0 0 0-.74.739l-1.04 4.35a.5.5 0 0 1-.95.074z" />
</svg>
);
const DashboardOverview = ({ business, stats }: { business: Business, stats: { totalViews: number, contactClicks: number } | null }) => (
interface DayStat {
date: string;
views: number;
clicks: number;
}
const CustomTooltip = ({ active, payload, label, color }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-white p-3 shadow-xl border border-gray-100 rounded-lg">
<p className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">{label}</p>
<p className="text-lg font-bold" style={{ color }}>
{payload[0].value} {payload[0].name === 'views' ? 'Vues' : 'Clics'}
</p>
</div>
);
}
return null;
};
const DashboardOverview = ({ business, stats }: {
business: Business,
stats: {
totalViews: number,
contactClicks: number,
dailyStats: DayStat[]
} | null
}) => (
<div className="space-y-6">
<h2 className="text-2xl font-bold font-serif text-gray-900">Tableau de bord</h2>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-3">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="bg-white overflow-hidden shadow-sm border border-gray-100 rounded-xl">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0 bg-brand-100 rounded-md p-3">
<div className="flex-shrink-0 bg-brand-50 rounded-lg p-3">
<Eye className="h-6 w-6 text-brand-600" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Vues de la fiche</dt>
<dd>
<div className="text-lg font-medium text-gray-900">{stats ? stats.totalViews : business.viewCount}</div>
<dd className="flex items-baseline">
<div className="text-2xl font-bold text-gray-900">{stats ? stats.totalViews : business.viewCount}</div>
</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="bg-white overflow-hidden shadow-sm border border-gray-100 rounded-xl">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0 bg-blue-100 rounded-md p-3">
<div className="flex-shrink-0 bg-blue-50 rounded-lg p-3">
<MousePointerClick className="h-6 w-6 text-blue-600" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Clics Contact</dt>
<dd>
<div className="text-lg font-medium text-gray-900">{stats ? stats.contactClicks : '...'}</div>
<dd className="flex items-baseline">
<div className="text-2xl font-bold text-gray-900">{stats ? stats.contactClicks : '...'}</div>
</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="bg-white overflow-hidden shadow-sm border border-gray-100 rounded-xl">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0 bg-yellow-100 rounded-md p-3">
<div className="flex-shrink-0 bg-yellow-50 rounded-lg p-3">
<Star className="h-6 w-6 text-yellow-600" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Note moyenne</dt>
<dd>
<div className="text-lg font-medium text-gray-900">{business.rating}/5</div>
<dd className="flex items-baseline">
<div className="text-2xl font-bold text-gray-900">{business.rating}/5</div>
</dd>
</dl>
</div>
@@ -66,11 +105,90 @@ const DashboardOverview = ({ business, stats }: { business: Business, stats: { t
</div>
</div>
<div className="bg-white shadow rounded-lg p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900 mb-4">Performances 30 derniers jours</h3>
<div className="h-64 bg-gray-50 rounded border border-dashed border-gray-200 flex items-center justify-center text-gray-400">
Graphique des visites (À venir en Phase 2)
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white shadow-sm border border-gray-100 rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-bold text-gray-900">Historique des Visites</h3>
<div className="px-2 py-1 bg-brand-50 text-brand-700 text-[10px] font-bold uppercase rounded leading-none">30 derniers jours</div>
</div>
<div className="h-72 w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={stats?.dailyStats || []}>
<defs>
<linearGradient id="colorViews" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#d97706" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#d97706" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f3f4f6" />
<XAxis
dataKey="date"
axisLine={false}
tickLine={false}
tick={{fill: '#9ca3af', fontSize: 10}}
interval={6}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{fill: '#9ca3af', fontSize: 10}}
/>
<Tooltip content={<CustomTooltip color="#d97706" />} />
<Area
type="monotone"
dataKey="views"
name="views"
stroke="#d97706"
strokeWidth={3}
fillOpacity={1}
fill="url(#colorViews)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
<div className="bg-white shadow-sm border border-gray-100 rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-bold text-gray-900">Interactions Contact</h3>
<div className="px-2 py-1 bg-blue-50 text-blue-700 text-[10px] font-bold uppercase rounded leading-none">30 derniers jours</div>
</div>
<div className="h-72 w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={stats?.dailyStats || []}>
<defs>
<linearGradient id="colorClicks" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#2563eb" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#2563eb" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f3f4f6" />
<XAxis
dataKey="date"
axisLine={false}
tickLine={false}
tick={{fill: '#9ca3af', fontSize: 10}}
interval={6}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{fill: '#9ca3af', fontSize: 10}}
/>
<Tooltip content={<CustomTooltip color="#2563eb" />} />
<Area
type="monotone"
dataKey="clicks"
name="clicks"
stroke="#2563eb"
strokeWidth={3}
fillOpacity={1}
fill="url(#colorClicks)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
);

View File

@@ -3,7 +3,7 @@
import React, { useState } from 'react';
import { Image as ImageIcon, Sparkles, Youtube, X, Globe, Facebook, Linkedin, Instagram, CheckCircle, AlertCircle } from 'lucide-react';
import { Business, CATEGORIES } from '../../types';
import { Business, CATEGORIES, Country } from '../../types';
import { generateBusinessDescription } from '../../lib/geminiService';
import { toast } from 'react-hot-toast';
import { useUser } from '../UserProvider';
@@ -15,17 +15,56 @@ const getYouTubeId = (url: string) => {
return (match && match[2].length === 11) ? match[2] : null;
};
const normalizeBusinessData = (b: Business): Business => ({
...b,
name: b.name || 'Nouvelle Entreprise',
category: b.category || CATEGORIES[0],
description: b.description || '',
location: b.location || '',
countryId: b.countryId || '',
city: b.city || '',
contactEmail: b.contactEmail || '',
showEmail: b.showEmail ?? true,
showPhone: b.showPhone ?? true,
showSocials: b.showSocials ?? true,
slug: b.slug || '',
videoUrl: b.videoUrl || '',
contactPhone: b.contactPhone || '',
websiteUrl: b.websiteUrl || '',
socialLinks: {
facebook: b.socialLinks?.facebook || '',
linkedin: b.socialLinks?.linkedin || '',
instagram: b.socialLinks?.instagram || '',
website: b.socialLinks?.website || '',
}
});
const DashboardProfile = ({ business, setBusiness }: { business: Business, setBusiness: (b: Business) => void }) => {
const { login } = useUser();
const [formData, setFormData] = useState(business);
const [formData, setFormData] = useState<Business>(normalizeBusinessData(business));
const [countries, setCountries] = useState<Country[]>([]);
const [videoInput, setVideoInput] = useState(business.videoUrl || '');
const [videoPreviewId, setVideoPreviewId] = useState<string | null>(getYouTubeId(business.videoUrl || ''));
const [isGenerating, setIsGenerating] = useState(false);
const [isMounted, setIsMounted] = useState(false);
// Fetch countries
React.useEffect(() => {
const fetchCountries = async () => {
try {
const res = await fetch('/api/countries');
const data = await res.json();
if (!data.error) setCountries(data);
} catch (error) {
console.error('Error fetching countries:', error);
}
};
fetchCountries();
}, []);
// Sync formData when business prop changes (initial fetch)
React.useEffect(() => {
setFormData(business);
setFormData(normalizeBusinessData(business));
setVideoInput(business.videoUrl || '');
setVideoPreviewId(getYouTubeId(business.videoUrl || ''));
}, [business]);
@@ -91,6 +130,38 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
}
};
const handleDelete = async () => {
if (!window.confirm("⚠️ ATTENTION : Êtes-vous sûr de vouloir supprimer définitivement votre boutique ? Cette action supprimera également vos offres, vos messages et vos statistiques. Cette action est irréversible.")) {
return;
}
try {
const res = await fetch(`/api/businesses/${business.id}`, {
method: 'DELETE',
headers: {
'x-user-id': business.ownerId
}
});
const data = await res.json();
if (data.success) {
toast.success('Boutique supprimée avec succès.');
// Upgrade local storage user if role changed
const localUser = JSON.parse(localStorage.getItem('afro_user') || '{}');
if (localUser.id === business.ownerId) {
localUser.role = 'VISITOR';
localStorage.setItem('afro_user', JSON.stringify(localUser));
if (login) login(localUser);
}
setTimeout(() => window.location.href = '/dashboard', 1500);
} else {
toast.error(data.error || 'Erreur lors de la suppression');
}
} catch (error) {
console.error('Delete error:', error);
toast.error('Erreur réseau lors de la suppression');
}
};
const isNameOk = formData.name && formData.name !== "Nouvelle Entreprise";
const isDescOk = formData.description && formData.description.length >= 20;
const isLocOk = formData.location && formData.location !== "Ma Ville";
@@ -254,17 +325,35 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
<div className="bg-white shadow rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center"><Globe className="w-5 h-5 mr-2 text-blue-500" /> Coordonnées & Réseaux</h3>
<div className="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-6">
<label className="block text-sm font-medium text-gray-700 mb-1">Localisation (Ville, Pays)</label>
<div className="sm:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">Pays</label>
<select
name="countryId"
value={formData.countryId || ''}
onChange={handleInputChange}
className="block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:ring-brand-500 focus:border-brand-500 sm:text-sm"
>
<option value="">Sélectionner un pays</option>
{countries.map(c => (
<option key={c.id} value={c.id}>{c.flag} {c.name}</option>
))}
</select>
</div>
<div className="sm:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">Ville</label>
<input
type="text"
name="location"
value={formData.location}
name="city"
value={formData.city || ''}
onChange={handleInputChange}
placeholder="Ex: Abidjan, Côte d'Ivoire"
placeholder="Ex: Abidjan"
className="block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:ring-brand-500 focus:border-brand-500 sm:text-sm"
/>
</div>
<div className="sm:col-span-6 hidden">
{/* Keep legacy field for compatibility if needed, but hide it */}
<input type="hidden" name="location" value={`${formData.city || ''}, ${countries.find(c => c.id === formData.countryId)?.name || ''}`} />
</div>
<div className="sm:col-span-3">
<div className="flex items-center justify-between mb-1">
<label className="block text-sm font-medium text-gray-700">Email Contact</label>
@@ -335,6 +424,29 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
</div>
</div>
</div>
{/* D. Zone de Danger */}
<div className="bg-red-50 border border-red-200 shadow-sm rounded-lg p-6">
<div className="flex items-center gap-2 mb-4 text-red-800">
<AlertCircle className="w-5 h-5" />
<h3 className="text-lg font-bold">Zone de Danger</h3>
</div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<p className="text-sm font-bold text-red-900">Supprimer définitivement la boutique</p>
<p className="text-xs text-red-700 mt-1">
Une fois supprimée, votre boutique ne sera plus visible dans l'annuaire.
Toutes vos données (offres, messages, avis) seront perdues.
</p>
</div>
<button
onClick={handleDelete}
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 font-bold text-sm shadow-sm transition-all active:scale-95 whitespace-nowrap"
>
Supprimer ma boutique
</button>
</div>
</div>
</div>
);
};