From a4f85b0b7bea9b1255ecb7b87aa09682d9087d69 Mon Sep 17 00:00:00 2001 From: streaper2 Date: Sun, 22 Feb 2026 20:25:47 +0100 Subject: [PATCH] first commit --- .gitignore | 24 + App.tsx | 60 + README.md | 20 + components/BusinessCard.tsx | 132 + components/DirectoryHero.tsx | 94 + components/Footer.tsx | 42 + components/Navbar.tsx | 86 + components/PricingSection.tsx | 325 +++ components/dashboard/DashboardOffers.tsx | 192 ++ components/dashboard/DashboardOverview.tsx | 78 + components/dashboard/DashboardProfile.tsx | 185 ++ index.html | 54 + index.tsx | 15 + metadata.json | 5 + package-lock.json | 2816 ++++++++++++++++++++ package.json | 24 + pages/AfroLifePage.tsx | 108 + pages/BlogPage.tsx | 56 + pages/BlogPostPage.tsx | 97 + pages/BusinessDetailPage.tsx | 353 +++ pages/DashboardPage.tsx | 113 + pages/DirectoryPage.tsx | 122 + pages/HomePage.tsx | 95 + pages/InterviewDetailPage.tsx | 110 + pages/LoginPage.tsx | 64 + pages/SubscriptionPage.tsx | 36 + services/geminiService.ts | 57 + services/mockData.ts | 352 +++ tsconfig.json | 29 + types.ts | 103 + vite.config.ts | 23 + 31 files changed, 5870 insertions(+) create mode 100644 .gitignore create mode 100644 App.tsx create mode 100644 README.md create mode 100644 components/BusinessCard.tsx create mode 100644 components/DirectoryHero.tsx create mode 100644 components/Footer.tsx create mode 100644 components/Navbar.tsx create mode 100644 components/PricingSection.tsx create mode 100644 components/dashboard/DashboardOffers.tsx create mode 100644 components/dashboard/DashboardOverview.tsx create mode 100644 components/dashboard/DashboardProfile.tsx create mode 100644 index.html create mode 100644 index.tsx create mode 100644 metadata.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pages/AfroLifePage.tsx create mode 100644 pages/BlogPage.tsx create mode 100644 pages/BlogPostPage.tsx create mode 100644 pages/BusinessDetailPage.tsx create mode 100644 pages/DashboardPage.tsx create mode 100644 pages/DirectoryPage.tsx create mode 100644 pages/HomePage.tsx create mode 100644 pages/InterviewDetailPage.tsx create mode 100644 pages/LoginPage.tsx create mode 100644 pages/SubscriptionPage.tsx create mode 100644 services/geminiService.ts create mode 100644 services/mockData.ts create mode 100644 tsconfig.json create mode 100644 types.ts create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..c473a83 --- /dev/null +++ b/App.tsx @@ -0,0 +1,60 @@ + +import React, { useState } from 'react'; +import { HashRouter, Routes, Route } from 'react-router-dom'; +import { User } from './types'; +import { MOCK_USER } from './services/mockData'; + +// Layout +import Navbar from './components/Navbar'; +import Footer from './components/Footer'; + +// Pages +import HomePage from './pages/HomePage'; +import DirectoryPage from './pages/DirectoryPage'; +import BlogPage from './pages/BlogPage'; +import BlogPostPage from './pages/BlogPostPage'; +import BusinessDetailPage from './pages/BusinessDetailPage'; +import LoginPage from './pages/LoginPage'; +import DashboardPage from './pages/DashboardPage'; +import AfroLifePage from './pages/AfroLifePage'; +import InterviewDetailPage from './pages/InterviewDetailPage'; +import SubscriptionPage from './pages/SubscriptionPage'; + +const App = () => { + const [user, setUser] = useState(null); + + const handleLogin = () => { + setUser(MOCK_USER); + }; + + const handleLogout = () => { + setUser(null); + // Redirect handled by rendering logic + window.location.hash = '/'; + }; + + return ( + +
+ +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + : } /> + +
+
+
+
+ ); +}; + +export default App; diff --git a/README.md b/README.md new file mode 100644 index 0000000..84fa721 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1Hb3LjU1spRnNuk8yMiBYkrIwYFSfTa9j + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/components/BusinessCard.tsx b/components/BusinessCard.tsx new file mode 100644 index 0000000..fd5cf8b --- /dev/null +++ b/components/BusinessCard.tsx @@ -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 ( +
+ {/* Header with Image and Actions */} +
+ Couverture +
+ + {/* Action Buttons */} +
+ {business.contactPhone && ( + 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" + > + + + )} +
+ + + {isShareOpen && ( +
setIsShareOpen(false)}> +
Partager
+ + + + +
+ )} +
+
+ + {/* Logo */} +
+ {business.name} +
+ + +
+ + {/* Body */} +
+
+ + {business.name} + + {business.verified && } +
+

{business.category}

+
+ + {business.location} +
+

{business.description}

+ +
+
+ + {business.rating} + ({business.viewCount} vues) +
+ Voir la fiche +
+
+
+ ); +}; + +export default BusinessCard; diff --git a/components/DirectoryHero.tsx b/components/DirectoryHero.tsx new file mode 100644 index 0000000..f19e674 --- /dev/null +++ b/components/DirectoryHero.tsx @@ -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 ( +
+ {/* Geometric Background Pattern */} +
+ + + + +
+ +
+ {/* Main Card */} +
+ + {/* Left: Imagery (Founder + Logo) */} +
+ {featuredBusiness.founderImageUrl ? ( + {featuredBusiness.founderName} + ) : ( +
+ +
+ )} + +
+ + {/* Overlaid Logo */} +
+
+ Logo +
+
+
+ + {/* Right: Content */} +
+
+ + Entrepreneur du mois + +
+ +

+ {featuredBusiness.name} +

+

+ Dirigé par {featuredBusiness.founderName} +

+ +
+
{featuredBusiness.category}
+
{featuredBusiness.location}
+
+ + {/* Shocking Figure */} + {featuredBusiness.keyMetric && ( +
+

{featuredBusiness.keyMetric}

+

Performance validée

+
+ )} + +
+ + Voir la fiche complète + + +
+
+
+
+
+ ); +}; + +export default DirectoryHero; diff --git a/components/Footer.tsx b/components/Footer.tsx new file mode 100644 index 0000000..3cb0b7b --- /dev/null +++ b/components/Footer.tsx @@ -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 ( +
+
+
+
+

Afropreunariat

+

+ La plateforme de référence pour l'entrepreneuriat africain. + Visibilité, connexion et croissance pour les TPE/PME. +

+
+
+

Liens Rapides

+
    +
  • Rechercher une entreprise
  • +
  • Inscrire mon entreprise
  • +
  • Actualités & Conseils
  • +
+
+
+

Contact

+

Abidjan, Côte d'Ivoire / Paris, France

+

support@afropreunariat.com

+
+
+
+ © 2025 Afropreunariat. Tous droits réservés. +
+
+
+ ); +}; + +export default Footer; diff --git a/components/Navbar.tsx b/components/Navbar.tsx new file mode 100644 index 0000000..5bc588e --- /dev/null +++ b/components/Navbar.tsx @@ -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 = ({ 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 ( + + ); +}; + +export default Navbar; diff --git a/components/PricingSection.tsx b/components/PricingSection.tsx new file mode 100644 index 0000000..d21e263 --- /dev/null +++ b/components/PricingSection.tsx @@ -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(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 ( +
+
+
+

Tarifs

+

+ Investissez dans votre croissance +

+

+ Choisissez le plan adapté à vos ambitions. Changez ou annulez à tout moment. +

+
+ + {/* Billing Toggle */} +
+
+ + +
+
+ + {/* Plans Grid */} +
+ {PLANS.map((plan) => ( +
+ {plan.recommended && ( +
+ + Populaire + +
+ )} +
+

{plan.name}

+

{plan.description}

+
+ + {plan.priceXOF === 'Gratuit' ? 'Gratuit' : (billingCycle === 'yearly' && plan.id !== 'starter' ? 'Sur Devis' : plan.priceXOF)} + + {plan.priceXOF !== 'Gratuit' && /mois} +
+ {plan.priceXOF !== 'Gratuit' && ( +

soit env. {plan.priceEUR} /mois

+ )} + +
    + {plan.features.map((feature) => ( +
  • +
    + +
    +

    {feature}

    +
  • + ))} +
+
+
+ +
+
+ ))} +
+
+ + {/* FAKE PAYMENT MODAL */} + {isPaymentModalOpen && selectedPlan && ( +
+
+ {/* Overlay */} +
+ + + +
+
+ + {/* Header */} +
+ + +
+ + {/* Content based on step */} + {paymentStep === 'method' && ( + <> +
+

Vous avez choisi le plan {selectedPlan.name}

+

{selectedPlan.priceXOF}

+
+ +

Moyen de paiement

+
+ + + +
+ + {paymentMethod === 'mobile_money' && ( +
+ + +
+ )} + {paymentMethod === 'card' && ( +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ )} + + )} + + {paymentStep === 'processing' && ( +
+ +

Traitement en cours...

+

Veuillez valider la transaction sur votre mobile si nécessaire.

+
+ )} + + {paymentStep === 'success' && ( +
+
+ +
+

Paiement Réussi !

+

Votre abonnement {selectedPlan.name} est maintenant actif.

+ +
+ )} +
+ + {paymentStep === 'method' && ( +
+ + +
+ )} +
+
+
+ )} +
+ ); +}; + +export default PricingSection; diff --git a/components/dashboard/DashboardOffers.tsx b/components/dashboard/DashboardOffers.tsx new file mode 100644 index 0000000..5ca9a1a --- /dev/null +++ b/components/dashboard/DashboardOffers.tsx @@ -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(MOCK_OFFERS); + const [isModalOpen, setIsModalOpen] = useState(false); + const [currentOffer, setCurrentOffer] = useState>({ + 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 ( +
+
+

Mes Offres

+ +
+ + {/* List View */} + {offers.length > 0 ? ( +
+ {offers.map(offer => ( +
+
+ {offer.title} +
+ + {offer.type === OfferType.PRODUCT ? 'Produit' : 'Service'} + +
+
+
+

{offer.title}

+

+ {new Intl.NumberFormat('fr-FR').format(offer.price)} {offer.currency} +

+
+ + {offer.active ? 'Actif' : 'Inactif'} + +
+ + +
+
+
+
+ ))} +
+ ) : ( +
+ +

Aucune offre

+

Commencez à vendre vos produits ou services.

+
+ )} + + {/* Modal / Drawer */} + {isModalOpen && ( +
+
+ + +
+
+
+
+
+ +
+
+ +
+
+ +
+ + +
+
+
+ + setCurrentOffer({...currentOffer, title: e.target.value})} placeholder="Ex: Savon Karité Bio" /> +
+
+
+ + setCurrentOffer({...currentOffer, price: parseInt(e.target.value)})} /> +
+
+ + +
+
+
+ +
+
+ +

PNG, JPG jusqu'à 5MB

+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ )} +
+ ); +}; + +export default DashboardOffers; diff --git a/components/dashboard/DashboardOverview.tsx b/components/dashboard/DashboardOverview.tsx new file mode 100644 index 0000000..35286f9 --- /dev/null +++ b/components/dashboard/DashboardOverview.tsx @@ -0,0 +1,78 @@ + +import React from 'react'; +import { Eye, Star } from 'lucide-react'; +import { Business } from '../../types'; + +const MousePointerClick = ({className}: {className?:string}) => ( + + + +); + +const DashboardOverview = ({ business }: { business: Business }) => ( +
+

Tableau de bord

+
+
+
+
+
+ +
+
+
+
Vues de la fiche
+
+
{business.viewCount}
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Clics Contact
+
+
42
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Note moyenne
+
+
{business.rating}/5
+
+
+
+
+
+
+
+ +
+

Performances 30 derniers jours

+
+ Graphique des visites (À venir en Phase 2) +
+
+
+); + +export default DashboardOverview; diff --git a/components/dashboard/DashboardProfile.tsx b/components/dashboard/DashboardProfile.tsx new file mode 100644 index 0000000..66bbc52 --- /dev/null +++ b/components/dashboard/DashboardProfile.tsx @@ -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(getYouTubeId(business.videoUrl || '')); + const [isGenerating, setIsGenerating] = useState(false); + + const handleInputChange = (e: React.ChangeEvent) => { + 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) => { + 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 ( +
+
+

Éditer mon profil

+ +
+ + {/* A. Bloc Identité Visuelle */} +
+

Identité Visuelle

+
+
+ Logo actuel +
+
+ +

Glissez votre logo ici ou cliquez pour parcourir

+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+