first commit
This commit is contained in:
132
components/BusinessCard.tsx
Normal file
132
components/BusinessCard.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { CheckCircle, MapPin, Star, Phone, Share2, Facebook, Linkedin, Instagram, Twitter } from 'lucide-react';
|
||||
import { Business } from '../types';
|
||||
|
||||
const BusinessCard: React.FC<{ business: Business }> = ({ business }) => {
|
||||
const [isShareOpen, setIsShareOpen] = useState(false);
|
||||
|
||||
const toggleShare = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsShareOpen(!isShareOpen);
|
||||
};
|
||||
|
||||
const handleShare = (platform: string) => {
|
||||
const url = encodeURIComponent(`${window.location.origin}/#/directory/${business.id}`);
|
||||
const text = encodeURIComponent(`Découvrez ${business.name} sur Afropreunariat`);
|
||||
|
||||
let shareLink = '';
|
||||
switch(platform) {
|
||||
case 'facebook':
|
||||
shareLink = `https://www.facebook.com/sharer/sharer.php?u=${url}`;
|
||||
break;
|
||||
case 'twitter':
|
||||
shareLink = `https://twitter.com/intent/tweet?url=${url}&text=${text}`;
|
||||
break;
|
||||
case 'linkedin':
|
||||
shareLink = `https://www.linkedin.com/sharing/share-offsite/?url=${url}`;
|
||||
break;
|
||||
case 'instagram':
|
||||
// Instagram web share is limited, alerting user
|
||||
alert("Pour partager sur Instagram, copiez le lien ou faites une capture d'écran.");
|
||||
setIsShareOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shareLink) {
|
||||
window.open(shareLink, '_blank', 'width=600,height=400');
|
||||
}
|
||||
setIsShareOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm hover:shadow-md transition-shadow duration-200 border border-gray-100 overflow-hidden flex flex-col h-full group relative">
|
||||
{/* Header with Image and Actions */}
|
||||
<div className="relative h-40 bg-gray-200">
|
||||
<img
|
||||
src={`https://picsum.photos/seed/${business.id}/500/300`}
|
||||
alt="Couverture"
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="absolute top-2 right-2 flex gap-2 z-20">
|
||||
{business.contactPhone && (
|
||||
<a
|
||||
href={`tel:${business.contactPhone}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="p-2 bg-white/20 backdrop-blur-md border border-white/20 rounded-full text-white hover:bg-white hover:text-brand-600 transition-all shadow-sm"
|
||||
title="Appeler"
|
||||
>
|
||||
<Phone className="w-4 h-4" />
|
||||
</a>
|
||||
)}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={toggleShare}
|
||||
className="p-2 bg-white/20 backdrop-blur-md border border-white/20 rounded-full text-white hover:bg-white hover:text-brand-600 transition-all shadow-sm"
|
||||
title="Partager"
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{isShareOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-xl py-2 border border-gray-100 z-30 animate-in fade-in zoom-in duration-200" onMouseLeave={() => setIsShareOpen(false)}>
|
||||
<div className="px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider">Partager</div>
|
||||
<button onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleShare('facebook'); }} className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 flex items-center hover:text-blue-600">
|
||||
<Facebook className="w-4 h-4 mr-3" /> Facebook
|
||||
</button>
|
||||
<button onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleShare('twitter'); }} className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 flex items-center hover:text-black">
|
||||
<Twitter className="w-4 h-4 mr-3" /> X (Twitter)
|
||||
</button>
|
||||
<button onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleShare('linkedin'); }} className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 flex items-center hover:text-blue-700">
|
||||
<Linkedin className="w-4 h-4 mr-3" /> LinkedIn
|
||||
</button>
|
||||
<button onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleShare('instagram'); }} className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 flex items-center hover:text-pink-600">
|
||||
<Instagram className="w-4 h-4 mr-3" /> Instagram
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="absolute -bottom-6 left-4 z-10 w-16 h-16 bg-white rounded-lg p-1 shadow-md">
|
||||
<img src={business.logoUrl} alt={business.name} className="w-full h-full object-cover rounded-md bg-gray-50" />
|
||||
</div>
|
||||
|
||||
<Link to={`/directory/${business.id}`} className="absolute inset-0 z-0" aria-label={`Voir ${business.name}`} />
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="pt-8 px-4 pb-4 flex-1 flex flex-col">
|
||||
<div className="flex justify-between items-start">
|
||||
<Link to={`/directory/${business.id}`} className="font-bold text-lg text-gray-900 truncate hover:text-brand-600 transition-colors">
|
||||
{business.name}
|
||||
</Link>
|
||||
{business.verified && <CheckCircle className="w-5 h-5 text-blue-500 flex-shrink-0 ml-1" />}
|
||||
</div>
|
||||
<p className="text-xs text-brand-600 font-semibold mb-2 uppercase tracking-wide">{business.category}</p>
|
||||
<div className="flex items-center text-sm text-gray-500 mb-3">
|
||||
<MapPin className="w-4 h-4 mr-1" />
|
||||
{business.location}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 line-clamp-2 mb-4 flex-1">{business.description}</p>
|
||||
|
||||
<div className="mt-auto flex items-center justify-between pt-4 border-t border-gray-50">
|
||||
<div className="flex items-center">
|
||||
<Star className="w-4 h-4 text-yellow-400 fill-current" />
|
||||
<span className="text-sm font-bold ml-1 text-gray-700">{business.rating}</span>
|
||||
<span className="text-xs text-gray-400 ml-1">({business.viewCount} vues)</span>
|
||||
</div>
|
||||
<Link to={`/directory/${business.id}`} className="text-brand-600 text-sm font-medium hover:underline">Voir la fiche</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessCard;
|
||||
94
components/DirectoryHero.tsx
Normal file
94
components/DirectoryHero.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { User as UserIcon, Award, Briefcase, MapPin, ArrowRight } from 'lucide-react';
|
||||
import { Business } from '../types';
|
||||
|
||||
const DirectoryHero = ({ featuredBusiness }: { featuredBusiness: Business }) => {
|
||||
if (!featuredBusiness) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 relative overflow-hidden pb-12 pt-6">
|
||||
{/* Geometric Background Pattern */}
|
||||
<div className="absolute inset-0 z-0 opacity-10">
|
||||
<svg className="h-full w-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<path d="M0 100 C 20 0 50 0 100 100 Z" fill="#ea580c" />
|
||||
<path d="M0 0 C 50 100 80 100 100 0 Z" fill="#ea580c" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Main Card */}
|
||||
<div className="bg-white rounded-2xl shadow-2xl overflow-hidden flex flex-col md:flex-row border border-gray-100">
|
||||
|
||||
{/* Left: Imagery (Founder + Logo) */}
|
||||
<div className="md:w-2/5 relative h-64 md:h-auto bg-gray-900 group">
|
||||
{featuredBusiness.founderImageUrl ? (
|
||||
<img
|
||||
src={featuredBusiness.founderImageUrl}
|
||||
alt={featuredBusiness.founderName}
|
||||
className="w-full h-full object-cover opacity-90 group-hover:opacity-100 transition-opacity duration-500"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-gradient-to-br from-gray-800 to-black flex items-center justify-center">
|
||||
<UserIcon className="w-20 h-20 text-gray-600" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent"></div>
|
||||
|
||||
{/* Overlaid Logo */}
|
||||
<div className="absolute bottom-6 left-6 flex items-center space-x-3">
|
||||
<div className="w-16 h-16 bg-white rounded-xl p-1 shadow-lg rotate-3 transform transition-transform group-hover:rotate-0">
|
||||
<img src={featuredBusiness.logoUrl} alt="Logo" className="w-full h-full object-cover rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Content */}
|
||||
<div className="md:w-3/5 p-8 md:p-12 flex flex-col justify-center">
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-yellow-100 text-yellow-800 uppercase tracking-wider">
|
||||
<Award className="w-3 h-3 mr-1" /> Entrepreneur du mois
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-serif text-gray-900 mb-2">
|
||||
{featuredBusiness.name}
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 mb-4 font-medium">
|
||||
Dirigé par <span className="text-brand-600">{featuredBusiness.founderName}</span>
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-4 text-sm text-gray-500 mb-6">
|
||||
<div className="flex items-center"><Briefcase className="w-4 h-4 mr-1"/> {featuredBusiness.category}</div>
|
||||
<div className="flex items-center"><MapPin className="w-4 h-4 mr-1"/> {featuredBusiness.location}</div>
|
||||
</div>
|
||||
|
||||
{/* Shocking Figure */}
|
||||
{featuredBusiness.keyMetric && (
|
||||
<div className="mb-8 p-4 bg-brand-50 border-l-4 border-brand-500 rounded-r-lg">
|
||||
<p className="text-2xl font-bold text-brand-900">{featuredBusiness.keyMetric}</p>
|
||||
<p className="text-xs text-brand-700 uppercase font-semibold">Performance validée</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Link
|
||||
to={`/directory/${featuredBusiness.id}`}
|
||||
className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-brand-600 hover:bg-brand-700 shadow-lg hover:shadow-xl transition-all"
|
||||
>
|
||||
Voir la fiche complète
|
||||
</Link>
|
||||
<button className="inline-flex items-center justify-center px-6 py-3 border-2 border-gray-200 text-base font-medium rounded-md text-gray-600 bg-transparent hover:border-brand-300 hover:text-brand-600 transition-colors">
|
||||
Devenir l'entrepreneur du mois <ArrowRight className="w-4 h-4 ml-2"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DirectoryHero;
|
||||
42
components/Footer.tsx
Normal file
42
components/Footer.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
const Footer = () => {
|
||||
const location = useLocation();
|
||||
if (location.pathname.startsWith('/dashboard')) return null;
|
||||
|
||||
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">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div>
|
||||
<h3 className="text-2xl font-serif font-bold text-brand-500 mb-4">Afropreunariat</h3>
|
||||
<p className="text-gray-400 text-sm">
|
||||
La plateforme de référence pour l'entrepreneuriat africain.
|
||||
Visibilité, connexion et croissance pour les TPE/PME.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-lg mb-4">Liens Rapides</h4>
|
||||
<ul className="space-y-2 text-gray-400 text-sm">
|
||||
<li><Link to="/directory" className="hover:text-brand-500">Rechercher une entreprise</Link></li>
|
||||
<li><Link to="/login" className="hover:text-brand-500">Inscrire mon entreprise</Link></li>
|
||||
<li><Link to="/blog" className="hover:text-brand-500">Actualités & Conseils</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-lg mb-4">Contact</h4>
|
||||
<p className="text-gray-400 text-sm mb-2">Abidjan, Côte d'Ivoire / Paris, France</p>
|
||||
<p className="text-gray-400 text-sm">support@afropreunariat.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 pt-8 border-t border-gray-800 text-center text-gray-500 text-sm">
|
||||
© 2025 Afropreunariat. Tous droits réservés.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
86
components/Navbar.tsx
Normal file
86
components/Navbar.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Menu, X, LayoutDashboard, User as UserIcon } from 'lucide-react';
|
||||
import { User } from '../types';
|
||||
|
||||
interface NavbarProps {
|
||||
user: User | null;
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
const Navbar: React.FC<NavbarProps> = ({ user, onLogout }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
// Hide Navbar on Dashboard to prevent double navigation, show only on public pages
|
||||
if (location.pathname.startsWith('/dashboard')) return null;
|
||||
|
||||
return (
|
||||
<nav className="bg-white shadow-sm sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex items-center">
|
||||
<Link to="/" 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>
|
||||
</Link>
|
||||
<div className="hidden sm:ml-8 sm:flex sm:space-x-8">
|
||||
<Link to="/" className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${location.pathname === '/' ? 'border-brand-500 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}`}>
|
||||
Accueil
|
||||
</Link>
|
||||
<Link to="/directory" className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${location.pathname.startsWith('/directory') ? 'border-brand-500 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}`}>
|
||||
Annuaire
|
||||
</Link>
|
||||
<Link to="/afrolife" className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${location.pathname.startsWith('/afrolife') ? 'border-brand-500 text-brand-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}`}>
|
||||
Afro Life
|
||||
</Link>
|
||||
<Link to="/blog" className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${location.pathname.startsWith('/blog') ? 'border-brand-500 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}`}>
|
||||
Blog
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden sm:ml-6 sm:flex sm:items-center space-x-4">
|
||||
{user ? (
|
||||
<>
|
||||
<Link to="/dashboard" className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-brand-600 hover:bg-brand-700 focus:outline-none">
|
||||
<LayoutDashboard className="w-4 h-4 mr-2" />
|
||||
Mon Espace
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<Link to="/login" className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none">
|
||||
<UserIcon className="w-4 h-4 mr-2" />
|
||||
Connexion / Inscription
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="-mr-2 flex items-center sm:hidden">
|
||||
<button onClick={() => setIsOpen(!isOpen)} className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none">
|
||||
{isOpen ? <X className="block h-6 w-6" /> : <Menu className="block h-6 w-6" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{isOpen && (
|
||||
<div className="sm:hidden bg-white border-t">
|
||||
<div className="pt-2 pb-3 space-y-1">
|
||||
<Link to="/" 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 to="/directory" 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 to="/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 to="/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>
|
||||
{user ? (
|
||||
<Link to="/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>
|
||||
) : (
|
||||
<Link to="/login" 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)}>Connexion</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
325
components/PricingSection.tsx
Normal file
325
components/PricingSection.tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Check, X, Smartphone, CreditCard, Loader, ShieldCheck } from 'lucide-react';
|
||||
|
||||
interface Plan {
|
||||
id: string;
|
||||
name: string;
|
||||
priceXOF: string;
|
||||
priceEUR: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
recommended?: boolean;
|
||||
color: string;
|
||||
}
|
||||
|
||||
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 [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
|
||||
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
|
||||
const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
|
||||
|
||||
// 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
|
||||
setSelectedPlan(plan);
|
||||
setPaymentStep('method');
|
||||
setIsPaymentModalOpen(true);
|
||||
};
|
||||
|
||||
const handlePayment = () => {
|
||||
setPaymentStep('processing');
|
||||
// Fake API delay
|
||||
setTimeout(() => {
|
||||
setPaymentStep('success');
|
||||
}, 2500);
|
||||
};
|
||||
|
||||
const closePayment = () => {
|
||||
setIsPaymentModalOpen(false);
|
||||
setPaymentStep('method');
|
||||
setSelectedPlan(null);
|
||||
setPaymentMethod(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-12 bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-base font-semibold text-brand-600 tracking-wide uppercase">Tarifs</h2>
|
||||
<p className="mt-1 text-4xl font-extrabold text-gray-900 sm:text-5xl sm:tracking-tight lg:text-6xl font-serif">
|
||||
Investissez dans votre croissance
|
||||
</p>
|
||||
<p className="max-w-xl mt-5 mx-auto text-xl text-gray-500">
|
||||
Choisissez le plan adapté à vos ambitions. Changez ou annulez à tout moment.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Billing Toggle */}
|
||||
<div className="mt-12 flex justify-center">
|
||||
<div className="relative bg-white rounded-lg p-0.5 flex sm:mt-0 shadow-sm border border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBillingCycle('monthly')}
|
||||
className={`${billingCycle === 'monthly' ? 'bg-gray-100 border-gray-200 shadow-sm text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700'} relative w-1/2 whitespace-nowrap py-2 px-6 rounded-md text-sm font-medium focus:outline-none transition-all sm:w-auto sm:px-8`}
|
||||
>
|
||||
Mensuel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBillingCycle('yearly')}
|
||||
className={`${billingCycle === 'yearly' ? 'bg-gray-100 border-gray-200 shadow-sm text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700'} relative w-1/2 whitespace-nowrap py-2 px-6 rounded-md text-sm font-medium focus:outline-none transition-all sm:w-auto sm:px-8`}
|
||||
>
|
||||
Annuel <span className="text-brand-600 text-xs ml-1 font-bold">-20%</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<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.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>
|
||||
|
||||
{/* FAKE PAYMENT MODAL */}
|
||||
{isPaymentModalOpen && selectedPlan && (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
{/* Overlay */}
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={closePayment}></div>
|
||||
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full">
|
||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 font-serif" id="modal-title">
|
||||
Paiement Sécurisé
|
||||
</h3>
|
||||
<button onClick={closePayment} className="text-gray-400 hover:text-gray-500">
|
||||
<X className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content based on step */}
|
||||
{paymentStep === 'method' && (
|
||||
<>
|
||||
<div className="bg-brand-50 p-4 rounded-md mb-6 border border-brand-100">
|
||||
<p className="text-sm text-brand-800 font-medium">Vous avez choisi le plan <span className="font-bold">{selectedPlan.name}</span></p>
|
||||
<p className="text-2xl font-bold text-brand-900 mt-1">{selectedPlan.priceXOF}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-sm font-medium text-gray-700 mb-3">Moyen de paiement</p>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<button
|
||||
onClick={() => setPaymentMethod('mobile_money')}
|
||||
className={`flex items-center p-4 border rounded-lg hover:bg-gray-50 transition-all ${paymentMethod === 'mobile_money' ? 'border-brand-500 ring-1 ring-brand-500 bg-brand-50' : 'border-gray-300'}`}
|
||||
>
|
||||
<div className="h-10 w-10 bg-orange-100 rounded-full flex items-center justify-center text-orange-600 mr-4">
|
||||
<Smartphone className="h-6 w-6" />
|
||||
</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 Money, MTN, Wave, Moov</p>
|
||||
</div>
|
||||
<div className="h-4 w-4 rounded-full border border-gray-300 flex items-center justify-center">
|
||||
{paymentMethod === 'mobile_money' && <div className="h-2 w-2 rounded-full bg-brand-600"></div>}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setPaymentMethod('card')}
|
||||
className={`flex items-center p-4 border rounded-lg hover:bg-gray-50 transition-all ${paymentMethod === 'card' ? 'border-brand-500 ring-1 ring-brand-500 bg-brand-50' : 'border-gray-300'}`}
|
||||
>
|
||||
<div className="h-10 w-10 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 mr-4">
|
||||
<CreditCard className="h-6 w-6" />
|
||||
</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>
|
||||
<div className="h-4 w-4 rounded-full border border-gray-300 flex items-center justify-center">
|
||||
{paymentMethod === 'card' && <div className="h-2 w-2 rounded-full bg-brand-600"></div>}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{paymentMethod === 'mobile_money' && (
|
||||
<div className="mt-4 animate-in fade-in slide-in-from-top-2">
|
||||
<label className="block text-sm font-medium text-gray-700">Numéro de téléphone</label>
|
||||
<input type="text" placeholder="+225 07..." className="mt-1 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>
|
||||
)}
|
||||
{paymentMethod === 'card' && (
|
||||
<div className="mt-4 animate-in fade-in slide-in-from-top-2 space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Numéro de carte</label>
|
||||
<input type="text" placeholder="0000 0000 0000 0000" className="mt-1 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="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Expiration</label>
|
||||
<input type="text" placeholder="MM/AA" className="mt-1 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>
|
||||
<label className="block text-sm font-medium text-gray-700">CVC</label>
|
||||
<input type="text" placeholder="123" className="mt-1 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>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{paymentStep === 'processing' && (
|
||||
<div className="py-10 text-center">
|
||||
<Loader className="h-12 w-12 text-brand-600 animate-spin mx-auto mb-4" />
|
||||
<p className="text-lg font-medium text-gray-900">Traitement en cours...</p>
|
||||
<p className="text-sm text-gray-500 mt-2">Veuillez valider la transaction sur votre mobile si nécessaire.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{paymentStep === 'success' && (
|
||||
<div className="py-6 text-center animate-in zoom-in duration-300">
|
||||
<div className="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-4">
|
||||
<ShieldCheck className="h-10 w-10 text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900">Paiement Réussi !</h3>
|
||||
<p className="text-gray-500 mt-2 mb-6">Votre abonnement {selectedPlan.name} est maintenant actif.</p>
|
||||
<button
|
||||
onClick={closePayment}
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-brand-600 text-base font-medium text-white hover:bg-brand-700 focus:outline-none sm:text-sm"
|
||||
>
|
||||
Accéder à mon espace
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{paymentStep === 'method' && (
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!paymentMethod}
|
||||
onClick={handlePayment}
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-brand-600 text-base font-medium text-white hover:bg-brand-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>
|
||||
Payer {selectedPlan.priceXOF}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closePayment}
|
||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingSection;
|
||||
192
components/dashboard/DashboardOffers.tsx
Normal file
192
components/dashboard/DashboardOffers.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { PlusCircle, Edit3, Trash2, ShoppingBag, Image as ImageIcon } from 'lucide-react';
|
||||
import { Offer, OfferType } from '../../types';
|
||||
import { MOCK_OFFERS } from '../../services/mockData';
|
||||
|
||||
const DashboardOffers = () => {
|
||||
const [offers, setOffers] = useState<Offer[]>(MOCK_OFFERS);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [currentOffer, setCurrentOffer] = useState<Partial<Offer>>({
|
||||
title: '',
|
||||
price: 0,
|
||||
currency: 'XOF',
|
||||
type: OfferType.SERVICE
|
||||
});
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (window.confirm("Voulez-vous vraiment supprimer cette offre ?")) {
|
||||
setOffers(offers.filter(o => o.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (offer: Offer) => {
|
||||
setCurrentOffer(offer);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const openAddModal = () => {
|
||||
setCurrentOffer({
|
||||
title: '',
|
||||
price: 0,
|
||||
currency: 'XOF',
|
||||
type: OfferType.SERVICE
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (currentOffer.id) {
|
||||
// Edit mode
|
||||
setOffers(offers.map(o => o.id === currentOffer.id ? { ...o, ...currentOffer } as Offer : o));
|
||||
} else {
|
||||
// Add mode
|
||||
const offer: Offer = {
|
||||
id: Date.now().toString(),
|
||||
businessId: '1',
|
||||
title: currentOffer.title || 'Nouvelle offre',
|
||||
type: currentOffer.type || OfferType.PRODUCT,
|
||||
price: currentOffer.price || 0,
|
||||
currency: currentOffer.currency as 'EUR'|'XOF',
|
||||
imageUrl: currentOffer.imageUrl || 'https://picsum.photos/300/200?random=' + Date.now(),
|
||||
active: true
|
||||
};
|
||||
setOffers([...offers, offer]);
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
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>
|
||||
<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">
|
||||
<PlusCircle className="w-4 h-4 mr-2" />
|
||||
Ajouter une offre
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* List View */}
|
||||
{offers.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{offers.map(offer => (
|
||||
<div key={offer.id} className="bg-white rounded-lg shadow border border-gray-200 overflow-hidden group">
|
||||
<div className="relative h-40 bg-gray-200">
|
||||
<img src={offer.imageUrl} alt={offer.title} className="w-full h-full object-cover" />
|
||||
<div className="absolute top-2 right-2">
|
||||
<span className={`px-2 py-1 text-xs font-bold rounded uppercase ${offer.type === OfferType.PRODUCT ? 'bg-blue-100 text-blue-800' : 'bg-purple-100 text-purple-800'}`}>
|
||||
{offer.type === OfferType.PRODUCT ? 'Produit' : 'Service'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="font-bold text-gray-900 truncate">{offer.title}</h3>
|
||||
<p className="text-brand-600 font-bold mt-1">
|
||||
{new Intl.NumberFormat('fr-FR').format(offer.price)} {offer.currency}
|
||||
</p>
|
||||
<div className="mt-4 flex justify-between items-center pt-4 border-t border-gray-100">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${offer.active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{offer.active ? 'Actif' : 'Inactif'}
|
||||
</span>
|
||||
<div className="flex space-x-2">
|
||||
<button onClick={() => handleEdit(offer)} className="p-1 text-gray-400 hover:text-brand-600" title="Modifier">
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(offer.id)} className="p-1 text-gray-400 hover:text-red-600" title="Supprimer">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 bg-white rounded-lg border-2 border-dashed border-gray-300">
|
||||
<ShoppingBag className="mx-auto h-12 w-12 text-gray-400" />
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">Aucune offre</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Commencez à vendre vos produits ou services.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal / Drawer */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true" onClick={() => setIsModalOpen(false)}></div>
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full">
|
||||
<form onSubmit={handleSave}>
|
||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-brand-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<PlusCircle className="h-6 w-6 text-brand-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||
{currentOffer.id ? 'Modifier l\'offre' : 'Ajouter une offre'}
|
||||
</h3>
|
||||
<div className="mt-4 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Type d'offre</label>
|
||||
<div className="mt-2 flex space-x-4">
|
||||
<label className="inline-flex items-center">
|
||||
<input type="radio" className="form-radio text-brand-600" name="type" checked={currentOffer.type === OfferType.PRODUCT} onChange={() => setCurrentOffer({...currentOffer, type: OfferType.PRODUCT})} />
|
||||
<span className="ml-2 text-sm">Produit Physique</span>
|
||||
</label>
|
||||
<label className="inline-flex items-center">
|
||||
<input type="radio" className="form-radio text-brand-600" name="type" checked={currentOffer.type === OfferType.SERVICE} onChange={() => setCurrentOffer({...currentOffer, type: OfferType.SERVICE})} />
|
||||
<span className="ml-2 text-sm">Service / Prestation</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Titre</label>
|
||||
<input type="text" required className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 sm:text-sm" value={currentOffer.title} onChange={e => setCurrentOffer({...currentOffer, title: e.target.value})} placeholder="Ex: Savon Karité Bio" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Prix</label>
|
||||
<input type="number" required min="0" className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 sm:text-sm" value={currentOffer.price} onChange={e => setCurrentOffer({...currentOffer, price: parseInt(e.target.value)})} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Devise</label>
|
||||
<select className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 sm:text-sm" value={currentOffer.currency} onChange={e => setCurrentOffer({...currentOffer, currency: e.target.value as any})}>
|
||||
<option value="XOF">FCFA (XOF)</option>
|
||||
<option value="EUR">EUR (€)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Photo</label>
|
||||
<div className="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
|
||||
<div className="space-y-1 text-center">
|
||||
<ImageIcon className="mx-auto h-12 w-12 text-gray-400" />
|
||||
<p className="text-xs text-gray-500">PNG, JPG jusqu'à 5MB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button type="submit" className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-brand-600 text-base font-medium text-white hover:bg-brand-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm">
|
||||
{currentOffer.id ? 'Mettre à jour' : 'Publier l\'offre'}
|
||||
</button>
|
||||
<button type="button" onClick={() => setIsModalOpen(false)} className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardOffers;
|
||||
78
components/dashboard/DashboardOverview.tsx
Normal file
78
components/dashboard/DashboardOverview.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Eye, Star } from 'lucide-react';
|
||||
import { Business } from '../../types';
|
||||
|
||||
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 }: { business: Business }) => (
|
||||
<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="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 bg-brand-100 rounded-md 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">{business.viewCount}</div>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 bg-blue-100 rounded-md 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">42</div>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 bg-yellow-100 rounded-md 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>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default DashboardOverview;
|
||||
185
components/dashboard/DashboardProfile.tsx
Normal file
185
components/dashboard/DashboardProfile.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Image as ImageIcon, Sparkles, Youtube, X, Globe, Facebook, Linkedin, Instagram } from 'lucide-react';
|
||||
import { Business, CATEGORIES } from '../../types';
|
||||
import { generateBusinessDescription } from '../../services/geminiService';
|
||||
|
||||
// Helper to extract youtube ID
|
||||
const getYouTubeId = (url: string) => {
|
||||
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
|
||||
const match = url.match(regExp);
|
||||
return (match && match[2].length === 11) ? match[2] : null;
|
||||
};
|
||||
|
||||
const DashboardProfile = ({ business, setBusiness }: { business: Business, setBusiness: (b: Business) => void }) => {
|
||||
const [formData, setFormData] = useState(business);
|
||||
const [videoInput, setVideoInput] = useState(business.videoUrl || '');
|
||||
const [videoPreviewId, setVideoPreviewId] = useState<string | null>(getYouTubeId(business.videoUrl || ''));
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleSocialChange = (network: keyof typeof formData.socialLinks, value: string) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
socialLinks: { ...formData.socialLinks, [network]: value }
|
||||
});
|
||||
};
|
||||
|
||||
const handleVideoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const url = e.target.value;
|
||||
setVideoInput(url);
|
||||
setVideoPreviewId(getYouTubeId(url));
|
||||
setFormData({ ...formData, videoUrl: url });
|
||||
};
|
||||
|
||||
const handleAiGenerate = async () => {
|
||||
setIsGenerating(true);
|
||||
const text = await generateBusinessDescription(formData.name, formData.category, "Innovation, Qualité, Service client");
|
||||
setFormData(prev => ({ ...prev, description: text }));
|
||||
setIsGenerating(false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setBusiness(formData);
|
||||
alert('Profil mis à jour avec succès !');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold font-serif text-gray-900">Éditer mon profil</h2>
|
||||
<button onClick={handleSave} className="bg-brand-600 text-white px-4 py-2 rounded-md hover:bg-brand-700 font-medium text-sm shadow-sm">
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* A. Bloc Identité Visuelle */}
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center"><ImageIcon className="w-5 h-5 mr-2 text-brand-600"/> Identité Visuelle</h3>
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="shrink-0">
|
||||
<img className="h-24 w-24 object-cover rounded-full border-2 border-gray-200" src={formData.logoUrl} alt="Logo actuel" />
|
||||
</div>
|
||||
<div className="flex-1 border-2 border-dashed border-gray-300 rounded-md p-6 flex flex-col items-center justify-center hover:border-brand-400 transition-colors cursor-pointer bg-gray-50">
|
||||
<ImageIcon className="h-8 w-8 text-gray-400" />
|
||||
<p className="mt-1 text-xs text-gray-500">Glissez votre logo ici ou cliquez pour parcourir</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6 mt-6">
|
||||
<div className="sm:col-span-3">
|
||||
<label className="block text-sm font-medium text-gray-700">Nom de l'entreprise</label>
|
||||
<input type="text" name="name" value={formData.name} onChange={handleInputChange} className="mt-1 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-3">
|
||||
<label className="block text-sm font-medium text-gray-700">Secteur d'activité</label>
|
||||
<select name="category" value={formData.category} onChange={handleInputChange} className="mt-1 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">
|
||||
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="sm:col-span-6">
|
||||
<div className="flex justify-between mb-1">
|
||||
<label className="block text-sm font-medium text-gray-700">Description</label>
|
||||
<button type="button" onClick={handleAiGenerate} disabled={isGenerating} className="text-xs flex items-center text-brand-600 hover:text-brand-800">
|
||||
{isGenerating ? '...' : <><Sparkles className="w-3 h-3 mr-1"/> Générer avec IA</>}
|
||||
</button>
|
||||
</div>
|
||||
<textarea name="description" rows={3} value={formData.description} 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" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* B. Bloc Présentation Vidéo */}
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center"><Youtube className="w-5 h-5 mr-2 text-red-600"/> Présentation Vidéo</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Lien de votre vidéo (Youtube)</label>
|
||||
<div className="mt-1 flex rounded-md shadow-sm">
|
||||
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm">
|
||||
https://
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
value={videoInput.replace('https://', '')}
|
||||
onChange={handleVideoChange}
|
||||
className="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md focus:ring-brand-500 focus:border-brand-500 sm:text-sm border-gray-300"
|
||||
placeholder="www.youtube.com/watch?v=..."
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500">Copiez l'URL de votre vidéo de présentation pour l'afficher sur votre profil.</p>
|
||||
</div>
|
||||
|
||||
{videoPreviewId ? (
|
||||
<div className="aspect-w-16 aspect-h-9 rounded-lg overflow-hidden bg-gray-100 mt-4 border border-gray-200">
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${videoPreviewId}`}
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
className="w-full h-64 sm:h-80 rounded-lg"
|
||||
></iframe>
|
||||
</div>
|
||||
) : videoInput && (
|
||||
<div className="rounded-md bg-red-50 p-4 mt-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<X className="h-5 w-5 text-red-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">Lien invalide</h3>
|
||||
<div className="mt-2 text-sm text-red-700">
|
||||
<p>Impossible de détecter une vidéo YouTube valide. Vérifiez le lien.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* C. Bloc Coordonnées & Réseaux */}
|
||||
<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-3">
|
||||
<label className="block text-sm font-medium text-gray-700">Email Contact</label>
|
||||
<input type="email" name="contactEmail" value={formData.contactEmail} onChange={handleInputChange} className="mt-1 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-3">
|
||||
<label className="block text-sm font-medium text-gray-700">Téléphone</label>
|
||||
<input type="text" name="contactPhone" value={formData.contactPhone || ''} onChange={handleInputChange} className="mt-1 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 border-t border-gray-100 pt-4 mt-2">
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-3 uppercase">Réseaux Sociaux</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500">
|
||||
<Facebook className="w-4 h-4"/>
|
||||
</span>
|
||||
<input type="text" placeholder="Lien Facebook" value={formData.socialLinks?.facebook || ''} onChange={(e) => handleSocialChange('facebook', e.target.value)} className="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md focus:ring-brand-500 focus:border-brand-500 sm:text-sm border-gray-300" />
|
||||
</div>
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500">
|
||||
<Linkedin className="w-4 h-4"/>
|
||||
</span>
|
||||
<input type="text" placeholder="Lien LinkedIn" value={formData.socialLinks?.linkedin || ''} onChange={(e) => handleSocialChange('linkedin', e.target.value)} className="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md focus:ring-brand-500 focus:border-brand-500 sm:text-sm border-gray-300" />
|
||||
</div>
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500">
|
||||
<Instagram className="w-4 h-4"/>
|
||||
</span>
|
||||
<input type="text" placeholder="Lien Instagram" value={formData.socialLinks?.instagram || ''} onChange={(e) => handleSocialChange('instagram', e.target.value)} className="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md focus:ring-brand-500 focus:border-brand-500 sm:text-sm border-gray-300" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardProfile;
|
||||
Reference in New Issue
Block a user