diff --git a/admin/next.config.ts b/admin/next.config.ts index 55238ac..c229ca5 100644 --- a/admin/next.config.ts +++ b/admin/next.config.ts @@ -1,8 +1,12 @@ import type { NextConfig } from "next"; +import path from "path"; const nextConfig: NextConfig = { /* config options here */ output: 'standalone', + turbopack: { + root: path.join(__dirname, ".."), + }, }; export default nextConfig; diff --git a/admin/package.json b/admin/package.json index fe1bb42..3c3fcdb 100644 --- a/admin/package.json +++ b/admin/package.json @@ -13,7 +13,6 @@ "@prisma/client": "6.19.3", "@prisma/config": "6.19.3", "@types/bcryptjs": "^2.4.6", - "afrohub": "file:..", "bcryptjs": "^3.0.3", "dotenv": "^17.4.1", "lucide-react": "^1.8.0", diff --git a/app/annuaire/[id]/page.tsx b/app/annuaire/[id]/page.tsx index 85af3c3..10c487e 100644 --- a/app/annuaire/[id]/page.tsx +++ b/app/annuaire/[id]/page.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; -import { ArrowLeft, MapPin, Globe, Mail, Phone, CheckCircle, Star, Facebook, Linkedin, Instagram, Twitter, Play, Share2, Send, Award, Quote, MessageCircle, Lock, Loader2, Link2, ShoppingBag } from 'lucide-react'; +import { ArrowLeft, MapPin, Globe, Mail, Phone, CheckCircle, Star, Facebook, Linkedin, Instagram, Twitter, Play, Share2, Send, Award, Quote, MessageCircle, Lock, Loader2, Link2, ShoppingBag, Heart } from 'lucide-react'; import { Business, OfferType, Rating } from '../../../types'; import { toast } from 'react-hot-toast'; import { useUser } from '../../../components/UserProvider'; @@ -28,6 +28,8 @@ const BusinessDetailPage = () => { const [replyingTo, setReplyingTo] = useState(null); const [replyText, setReplyText] = useState(''); const [isSubmittingReply, setIsSubmittingReply] = useState(false); + const [isFavorited, setIsFavorited] = useState(false); + const [isTogglingFavorite, setIsTogglingFavorite] = useState(false); const isOwner = user && business && user.id === business.ownerId; @@ -143,6 +145,21 @@ const BusinessDetailPage = () => { } finally { setLoadingRatings(false); } + + // 6. Fetch favorite status + if (user) { + try { + const favRes = await fetch('/api/favorites', { + headers: { 'x-user-id': user.id } + }); + const favData = await favRes.json(); + if (Array.isArray(favData)) { + setIsFavorited(favData.some((f: any) => f.businessId === id)); + } + } catch (e) { + console.error('Error fetching favorite status:', e); + } + } }; if (id) loadBusiness(); @@ -397,6 +414,35 @@ const BusinessDetailPage = () => { } }; + const toggleFavorite = async () => { + if (!user) { + toast.error("Connectez-vous pour ajouter des favoris"); + router.push('/login'); + return; + } + + setIsTogglingFavorite(true); + try { + const res = await fetch('/api/favorites', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-user-id': user.id + }, + body: JSON.stringify({ businessId: business?.id || id }) + }); + const data = await res.json(); + if (!data.error) { + setIsFavorited(data.favorited); + toast.success(data.favorited ? 'Ajouté aux favoris' : 'Retiré des favoris'); + } + } catch (error) { + toast.error('Erreur réseau'); + } finally { + setIsTogglingFavorite(false); + } + }; + return (
{/* Header Banner */} @@ -463,6 +509,7 @@ const BusinessDetailPage = () => { )}
+ {/* Actions block refreshed by AI coding assistant */}
)}
+ + + Contacter diff --git a/app/api/favorites/route.ts b/app/api/favorites/route.ts new file mode 100644 index 0000000..8487c19 --- /dev/null +++ b/app/api/favorites/route.ts @@ -0,0 +1,76 @@ +import { NextRequest, NextResponse } from 'next/server'; +import prisma from '@/lib/prisma'; + +// GET /api/favorites - Get user's favorites +export async function GET(request: NextRequest) { + try { + const userId = request.headers.get('x-user-id'); + if (!userId) { + return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }); + } + + const favorites = await prisma.favorite.findMany({ + where: { userId }, + include: { + business: { + include: { + categoryRef: true, + country: true + } + } + }, + orderBy: { createdAt: 'desc' } + }); + + return NextResponse.json(favorites); + } catch (error) { + console.error('GET /api/favorites error:', error); + return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 }); + } +} + +// POST /api/favorites - Toggle favorite +export async function POST(request: NextRequest) { + try { + const userId = request.headers.get('x-user-id'); + const body = await request.json(); + const { businessId } = body; + + if (!userId) { + return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }); + } + if (!businessId) { + return NextResponse.json({ error: 'businessId requis' }, { status: 400 }); + } + + // Check if already favorited + const existing = await prisma.favorite.findUnique({ + where: { + userId_businessId: { + userId, + businessId + } + } + }); + + if (existing) { + // Unfavorite + await prisma.favorite.delete({ + where: { id: existing.id } + }); + return NextResponse.json({ favorited: false }); + } else { + // Favorite + await prisma.favorite.create({ + data: { + userId, + businessId + } + }); + return NextResponse.json({ favorited: true }); + } + } catch (error) { + console.error('POST /api/favorites error:', error); + return NextResponse.json({ error: 'Erreur lors du changement de favori' }, { status: 500 }); + } +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 1fc66f5..20561c2 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, Suspense } from 'react'; import Link from 'next/link'; -import { LayoutDashboard, Edit3, ShoppingBag, CreditCard, LogOut, Eye, MessageSquare, User as UserIcon, Rocket, ShieldAlert } from 'lucide-react'; +import { LayoutDashboard, Edit3, ShoppingBag, CreditCard, LogOut, Eye, MessageSquare, User as UserIcon, Rocket, ShieldAlert, Heart } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; import { User } from '../../types'; import DashboardOverview from '../../components/dashboard/DashboardOverview'; @@ -12,6 +12,7 @@ import DashboardProfileClient from '../../components/dashboard/DashboardProfileC import DashboardMessages from '../../components/dashboard/DashboardMessages'; import DashboardOffers from '../../components/dashboard/DashboardOffers'; import PricingSection from '../../components/PricingSection'; +import DashboardFavorites from '../../components/dashboard/DashboardFavorites'; import { useUser } from '../../components/UserProvider'; const DashboardContent = () => { @@ -21,7 +22,7 @@ const DashboardContent = () => { const searchParams = useSearchParams(); const chatId = searchParams.get('chatId') || searchParams.get('chat'); - const [currentView, setCurrentView] = useState<'overview' | 'profile' | 'offers' | 'subscription' | 'messages' | 'personal-profile'>('overview'); + const [currentView, setCurrentView] = useState<'overview' | 'profile' | 'offers' | 'subscription' | 'messages' | 'personal-profile' | 'favorites'>('overview'); const [business, setBusiness] = useState(null); const [stats, setStats] = useState<{ totalViews: number, contactClicks: number, dailyStats: any[] } | null>(null); const [loading, setLoading] = useState(true); @@ -33,7 +34,7 @@ const DashboardContent = () => { // Handle view from URL const viewParam = searchParams.get('view'); - if (viewParam && ['overview', 'profile', 'offers', 'subscription', 'messages', 'personal-profile'].includes(viewParam)) { + if (viewParam && ['overview', 'profile', 'offers', 'subscription', 'messages', 'personal-profile', 'favorites'].includes(viewParam)) { setCurrentView(viewParam as any); } }, [searchParams]); @@ -220,6 +221,11 @@ const DashboardContent = () => { Mon Profil Client + +

Espace Business

@@ -267,6 +273,7 @@ const DashboardContent = () => { {currentView === 'overview' && 'Vue d\'ensemble'} {currentView === 'messages' && 'Mes Messages'} {currentView === 'personal-profile' && 'Mon Profil Client'} + {currentView === 'favorites' && 'Mes Favoris'} {currentView === 'profile' && (user.role === 'ENTREPRENEUR' ? 'Ma Fiche Entreprise' : 'Créer mon Business')} {currentView === 'offers' && 'Gestion des Offres'} {currentView === 'subscription' && 'Mon Abonnement'} @@ -302,6 +309,7 @@ const DashboardContent = () => { {currentView === 'overview' && } {currentView === 'messages' && } {currentView === 'personal-profile' && } + {currentView === 'favorites' && } {currentView === 'profile' && } {currentView === 'offers' && } {currentView === 'subscription' && ( diff --git a/components/BusinessCard.tsx b/components/BusinessCard.tsx index 9b80275..ccc55f3 100644 --- a/components/BusinessCard.tsx +++ b/components/BusinessCard.tsx @@ -3,12 +3,47 @@ import React, { useState } from 'react'; import Link from 'next/link'; -import { CheckCircle, MapPin, Star, Phone, Share2, Facebook, Linkedin, Instagram, Twitter } from 'lucide-react'; +import { CheckCircle, MapPin, Star, Phone, Share2, Facebook, Linkedin, Instagram, Twitter, Heart } from 'lucide-react'; +import { useUser } from './UserProvider'; import { Business } from '../types'; import { toast } from 'react-hot-toast'; const BusinessCard: React.FC<{ business: Business }> = ({ business }) => { + const { user } = useUser(); const [isShareOpen, setIsShareOpen] = useState(false); + const [isFavorited, setIsFavorited] = useState(false); + const [isToggling, setIsToggling] = useState(false); + + const toggleFavorite = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (!user) { + toast.error("Connectez-vous pour ajouter des favoris"); + return; + } + + setIsToggling(true); + try { + const res = await fetch('/api/favorites', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-user-id': user.id + }, + body: JSON.stringify({ businessId: business.id }) + }); + const data = await res.json(); + if (!data.error) { + setIsFavorited(data.favorited); + toast.success(data.favorited ? 'Ajouté aux favoris' : 'Retiré des favoris'); + } + } catch (error) { + toast.error('Erreur réseau'); + } finally { + setIsToggling(false); + } + }; const toggleShare = (e: React.MouseEvent) => { e.preventDefault(); @@ -70,6 +105,14 @@ const BusinessCard: React.FC<{ business: Business }> = ({ business }) => { )} +
+
+ +
+

{fav.business.name}

+

{fav.business.category}

+ +
+ + {fav.business.location} +
+ +
+
+ + {fav.business.rating} +
+ + Voir la fiche + + +
+
+ + ))} + + )} + + ); +}; + +export default DashboardFavorites; diff --git a/components/dashboard/DashboardOverview.tsx b/components/dashboard/DashboardOverview.tsx index ed7ac80..d4630d5 100644 --- a/components/dashboard/DashboardOverview.tsx +++ b/components/dashboard/DashboardOverview.tsx @@ -112,7 +112,7 @@ const DashboardOverview = ({ business, stats }: {
30 derniers jours
- + @@ -154,7 +154,7 @@ const DashboardOverview = ({ business, stats }: {
30 derniers jours
- + diff --git a/features.md b/features.md new file mode 100644 index 0000000..c546f20 --- /dev/null +++ b/features.md @@ -0,0 +1,35 @@ +# Propositions d'Améliorations et Nouvelles Fonctionnalités pour Afrohub + +Ce document propose une liste d'idées de fonctionnalités pour enrichir la plateforme Afrohub (Afropreunariat) et améliorer la proposition de valeur pour les visiteurs, les entrepreneurs et les administrateurs. Ces suggestions s'appuient sur l'architecture actuelle du projet (Prisma, Next.js). + +## 1. Fonctionnalités pour les Entrepreneurs (B2B & Croissance) +* **Tableau de bord Analytique Avancé** : Enrichir l'espace entrepreneur (notamment pour les plans BOOSTER et EMPIRE) avec des statistiques visuelles : évolution des vues du profil, taux de clics vers les réseaux sociaux/site web, et origine géographique des visiteurs (en exploitant le modèle `AnalyticsEvent`). +* **Portail d'Offres d'Emploi / Missions (Job Board)** : Permettre aux entreprises référencées de publier des offres d'emploi ou des missions freelance. Les utilisateurs pourraient postuler directement via la plateforme. +* **E-commerce & Paiement Intégré** : Évoluer le modèle `Offer` actuel pour permettre non seulement l'affichage, mais aussi l'achat direct de produits ou la réservation de services via des moyens de paiement locaux (Mobile Money, Paystack, etc.). +* **Mise en relation B2B (Matchmaking)** : Un outil permettant aux entreprises de rechercher des partenaires commerciaux ou des fournisseurs et de se faire des "Demandes de Partenariat" structurées. + +## 2. Fonctionnalités pour les Utilisateurs et l'Engagement Communautaire +* **Système de Favoris / Collections** : Permettre aux utilisateurs connectés de sauvegarder leurs entreprises, événements ou articles préférés dans des listes privées ou publiques (ex: "Mes restaurants préférés à Abidjan"). +* **Avis Enrichis (Médias)** : Améliorer le modèle `Rating` pour autoriser les utilisateurs à télécharger des photos (preuves d'achat, réalisation) lorsqu'ils laissent un avis sur une entreprise. +* **Billetterie et Inscription aux Événements (RSVP)** : Aller au-delà de la simple vitrine pour le modèle `Event` en permettant aux utilisateurs de confirmer leur présence, de recevoir des rappels par email, ou d'ajouter l'événement à leur calendrier personnel. +* **Forum ou Espace "Questions / Réponses"** : Créer un espace communautaire d'entraide où les jeunes entrepreneurs peuvent poser des questions (juridiques, marketing, etc.) et recevoir des réponses de la communauté ou d'experts certifiés. + +## 3. Outils de Financement et Monétisation +* **Hub Investisseurs / Levée de fonds** : Permettre aux entreprises d'indiquer qu'elles sont en recherche de financement et d'héberger un Pitch Deck. Un rôle spécifique `INVESTOR` pourrait être créé pour accéder à ces données sensibles. +* **Système de Mise en Avant Publicitaire (Ads)** : Offrir la possibilité aux entreprises d'acheter des "crédits" pour être sponsorisées dans les résultats de recherche ou dans la newsletter du site, en plus de la fonctionnalité `isFeatured` existante. + +## 4. Expérience Utilisateur (UX) et Technique +* **Recherche par Carte Géolocalisée** : Intégrer une vue "Carte" (Mapbox ou Google Maps) pour découvrir les entreprises et les événements situés à proximité de la position de l'utilisateur. +* **Support Multilingue (i18n)** : Étendre la portée de la plateforme en la rendant disponible en anglais, portugais et dans d'autres langues majeures du continent pour attirer un public panafricain et la diaspora. +* **Mentorat (Mentorship Program)** : Un algorithme ou un système de mise en relation liant des entrepreneurs expérimentés (Mentors) avec des porteurs de projets (Mentees). +* **Notifications Push / Progressive Web App (PWA)** : Transformer le site Web en PWA installable sur smartphone pour envoyer des notifications push lors de la réception d'un nouveau message (`Message`) ou de la publication d'un nouvel événement. + + +ajout pwa; +ajout de paiement ponctuel pour : + - créer un évement; + - remonter dans la fil de recherche + - mise en avant publicitaire + +permettre d'ajouter un entrepreneur en favoris. + diff --git a/next.config.mjs b/next.config.mjs index cde60d5..1d0e1ff 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,8 +1,17 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, output: 'standalone', poweredByHeader: false, + turbopack: { + root: __dirname, + }, async headers() { return [ { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e55ccb2..cb53eb1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -34,6 +34,7 @@ model User { reports MessageReport[] ratings Rating[] country Country? @relation(fields: [countryId], references: [id]) + favorites Favorite[] } model Business { @@ -82,6 +83,7 @@ model Business { conversations Conversation[] offers Offer[] ratings Rating[] + favorites Favorite[] } model BusinessCategory { @@ -317,6 +319,17 @@ model Event { tags String[] @default([]) } +model Favorite { + id String @id @default(uuid()) + userId String + businessId String + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + business Business @relation(fields: [businessId], references: [id], onDelete: Cascade) + + @@unique([userId, businessId]) +} + enum RatingStatus { PENDING APPROVED