ajout de la gestion des favoris
All checks were successful
Build and Push App / build (push) Successful in 13m38s
All checks were successful
Build and Push App / build (push) Successful in 13m38s
This commit is contained in:
@@ -1,8 +1,12 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
turbopack: {
|
||||||
|
root: path.join(__dirname, ".."),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
"@prisma/client": "6.19.3",
|
"@prisma/client": "6.19.3",
|
||||||
"@prisma/config": "6.19.3",
|
"@prisma/config": "6.19.3",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"afrohub": "file:..",
|
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"dotenv": "^17.4.1",
|
"dotenv": "^17.4.1",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
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 { Business, OfferType, Rating } from '../../../types';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { useUser } from '../../../components/UserProvider';
|
import { useUser } from '../../../components/UserProvider';
|
||||||
@@ -28,6 +28,8 @@ const BusinessDetailPage = () => {
|
|||||||
const [replyingTo, setReplyingTo] = useState<string | null>(null);
|
const [replyingTo, setReplyingTo] = useState<string | null>(null);
|
||||||
const [replyText, setReplyText] = useState('');
|
const [replyText, setReplyText] = useState('');
|
||||||
const [isSubmittingReply, setIsSubmittingReply] = useState(false);
|
const [isSubmittingReply, setIsSubmittingReply] = useState(false);
|
||||||
|
const [isFavorited, setIsFavorited] = useState(false);
|
||||||
|
const [isTogglingFavorite, setIsTogglingFavorite] = useState(false);
|
||||||
|
|
||||||
const isOwner = user && business && user.id === business.ownerId;
|
const isOwner = user && business && user.id === business.ownerId;
|
||||||
|
|
||||||
@@ -143,6 +145,21 @@ const BusinessDetailPage = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoadingRatings(false);
|
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();
|
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 (
|
return (
|
||||||
<div className="bg-gray-50 min-h-screen pb-12">
|
<div className="bg-gray-50 min-h-screen pb-12">
|
||||||
{/* Header Banner */}
|
{/* Header Banner */}
|
||||||
@@ -463,6 +509,7 @@ const BusinessDetailPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 relative">
|
<div className="flex gap-3 relative">
|
||||||
|
{/* Actions block refreshed by AI coding assistant */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsShareOpen(!isShareOpen)}
|
onClick={() => setIsShareOpen(!isShareOpen)}
|
||||||
@@ -517,6 +564,16 @@ const BusinessDetailPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={toggleFavorite}
|
||||||
|
disabled={isTogglingFavorite}
|
||||||
|
className={`inline-flex items-center px-4 py-2 border shadow-sm text-sm font-medium rounded-md transition-all active:scale-95 ${isFavorited ? 'bg-red-500 border-red-500 text-white' : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'}`}
|
||||||
|
>
|
||||||
|
<Heart className={`w-4 h-4 mr-2 ${isFavorited ? 'fill-current' : ''}`} />
|
||||||
|
{isFavorited ? 'Favori' : 'Mettre en favori'}
|
||||||
|
</button>
|
||||||
|
|
||||||
<a href="#contact-form" className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-brand-600 hover:bg-brand-700">
|
<a href="#contact-form" className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-brand-600 hover:bg-brand-700">
|
||||||
Contacter
|
Contacter
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
76
app/api/favorites/route.ts
Normal file
76
app/api/favorites/route.ts
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, Suspense } from 'react';
|
import React, { useState, useEffect, Suspense } from 'react';
|
||||||
import Link from 'next/link';
|
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 { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { User } from '../../types';
|
import { User } from '../../types';
|
||||||
import DashboardOverview from '../../components/dashboard/DashboardOverview';
|
import DashboardOverview from '../../components/dashboard/DashboardOverview';
|
||||||
@@ -12,6 +12,7 @@ import DashboardProfileClient from '../../components/dashboard/DashboardProfileC
|
|||||||
import DashboardMessages from '../../components/dashboard/DashboardMessages';
|
import DashboardMessages from '../../components/dashboard/DashboardMessages';
|
||||||
import DashboardOffers from '../../components/dashboard/DashboardOffers';
|
import DashboardOffers from '../../components/dashboard/DashboardOffers';
|
||||||
import PricingSection from '../../components/PricingSection';
|
import PricingSection from '../../components/PricingSection';
|
||||||
|
import DashboardFavorites from '../../components/dashboard/DashboardFavorites';
|
||||||
import { useUser } from '../../components/UserProvider';
|
import { useUser } from '../../components/UserProvider';
|
||||||
|
|
||||||
const DashboardContent = () => {
|
const DashboardContent = () => {
|
||||||
@@ -21,7 +22,7 @@ const DashboardContent = () => {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const chatId = searchParams.get('chatId') || searchParams.get('chat');
|
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<any>(null);
|
const [business, setBusiness] = useState<any>(null);
|
||||||
const [stats, setStats] = useState<{ totalViews: number, contactClicks: number, dailyStats: any[] } | null>(null);
|
const [stats, setStats] = useState<{ totalViews: number, contactClicks: number, dailyStats: any[] } | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -33,7 +34,7 @@ const DashboardContent = () => {
|
|||||||
|
|
||||||
// Handle view from URL
|
// Handle view from URL
|
||||||
const viewParam = searchParams.get('view');
|
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);
|
setCurrentView(viewParam as any);
|
||||||
}
|
}
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
@@ -220,6 +221,11 @@ const DashboardContent = () => {
|
|||||||
Mon Profil Client
|
Mon Profil Client
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button onClick={() => setCurrentView('favorites')} className={`${currentView === 'favorites' ? 'bg-brand-50 text-brand-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'} group flex items-center px-2 py-2 text-sm font-medium rounded-md w-full`}>
|
||||||
|
<Heart className={`${currentView === 'favorites' ? 'text-brand-500' : 'text-gray-400 group-hover:text-gray-500'} mr-3 flex-shrink-0 h-5 w-5`} />
|
||||||
|
Mes Favoris
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="pt-4 pb-2">
|
<div className="pt-4 pb-2">
|
||||||
<p className="px-3 text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Espace Business</p>
|
<p className="px-3 text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Espace Business</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -267,6 +273,7 @@ const DashboardContent = () => {
|
|||||||
{currentView === 'overview' && 'Vue d\'ensemble'}
|
{currentView === 'overview' && 'Vue d\'ensemble'}
|
||||||
{currentView === 'messages' && 'Mes Messages'}
|
{currentView === 'messages' && 'Mes Messages'}
|
||||||
{currentView === 'personal-profile' && 'Mon Profil Client'}
|
{currentView === 'personal-profile' && 'Mon Profil Client'}
|
||||||
|
{currentView === 'favorites' && 'Mes Favoris'}
|
||||||
{currentView === 'profile' && (user.role === 'ENTREPRENEUR' ? 'Ma Fiche Entreprise' : 'Créer mon Business')}
|
{currentView === 'profile' && (user.role === 'ENTREPRENEUR' ? 'Ma Fiche Entreprise' : 'Créer mon Business')}
|
||||||
{currentView === 'offers' && 'Gestion des Offres'}
|
{currentView === 'offers' && 'Gestion des Offres'}
|
||||||
{currentView === 'subscription' && 'Mon Abonnement'}
|
{currentView === 'subscription' && 'Mon Abonnement'}
|
||||||
@@ -302,6 +309,7 @@ const DashboardContent = () => {
|
|||||||
{currentView === 'overview' && <DashboardOverview business={displayBusiness} stats={stats} />}
|
{currentView === 'overview' && <DashboardOverview business={displayBusiness} stats={stats} />}
|
||||||
{currentView === 'messages' && <DashboardMessages onMessagesRead={fetchUnreadCount} initialConversationId={chatId} />}
|
{currentView === 'messages' && <DashboardMessages onMessagesRead={fetchUnreadCount} initialConversationId={chatId} />}
|
||||||
{currentView === 'personal-profile' && <DashboardProfileClient />}
|
{currentView === 'personal-profile' && <DashboardProfileClient />}
|
||||||
|
{currentView === 'favorites' && <DashboardFavorites />}
|
||||||
{currentView === 'profile' && <DashboardProfile business={displayBusiness} setBusiness={setBusiness} />}
|
{currentView === 'profile' && <DashboardProfile business={displayBusiness} setBusiness={setBusiness} />}
|
||||||
{currentView === 'offers' && <DashboardOffers businessId={displayBusiness.id} plan={displayBusiness.plan} />}
|
{currentView === 'offers' && <DashboardOffers businessId={displayBusiness.id} plan={displayBusiness.plan} />}
|
||||||
{currentView === 'subscription' && (
|
{currentView === 'subscription' && (
|
||||||
|
|||||||
@@ -3,12 +3,47 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Link from 'next/link';
|
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 { Business } from '../types';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
const BusinessCard: React.FC<{ business: Business }> = ({ business }) => {
|
const BusinessCard: React.FC<{ business: Business }> = ({ business }) => {
|
||||||
|
const { user } = useUser();
|
||||||
const [isShareOpen, setIsShareOpen] = useState(false);
|
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) => {
|
const toggleShare = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -70,6 +105,14 @@ const BusinessCard: React.FC<{ business: Business }> = ({ business }) => {
|
|||||||
<Phone className="w-4 h-4" />
|
<Phone className="w-4 h-4" />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
onClick={toggleFavorite}
|
||||||
|
disabled={isToggling}
|
||||||
|
className={`p-2 backdrop-blur-md border rounded-full transition-all shadow-sm ${isFavorited ? 'bg-red-500 border-red-500 text-white' : 'bg-white/20 border-white/20 text-white hover:bg-white hover:text-red-500'}`}
|
||||||
|
title={isFavorited ? "Retirer des favoris" : "Ajouter aux favoris"}
|
||||||
|
>
|
||||||
|
<Heart className={`w-4 h-4 ${isFavorited ? 'fill-current' : ''}`} />
|
||||||
|
</button>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={toggleShare}
|
onClick={toggleShare}
|
||||||
|
|||||||
171
components/dashboard/DashboardFavorites.tsx
Normal file
171
components/dashboard/DashboardFavorites.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Heart, Loader2, Search, MapPin, Star, ExternalLink } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useUser } from '../UserProvider';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
|
interface Favorite {
|
||||||
|
id: string;
|
||||||
|
business: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
location: string;
|
||||||
|
description: string;
|
||||||
|
logoUrl: string;
|
||||||
|
slug: string | null;
|
||||||
|
rating: number;
|
||||||
|
viewCount: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardFavorites = () => {
|
||||||
|
const { user } = useUser();
|
||||||
|
const [favorites, setFavorites] = useState<Favorite[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
fetchFavorites();
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const fetchFavorites = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/favorites', {
|
||||||
|
headers: { 'x-user-id': user?.id || '' }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.error) {
|
||||||
|
setFavorites(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching favorites:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFavorite = async (e: React.MouseEvent, businessId: string) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/favorites', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-user-id': user?.id || ''
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ businessId })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.error) {
|
||||||
|
setFavorites(favorites.filter(f => f.business.id !== businessId));
|
||||||
|
toast.success('Retiré des favoris');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Erreur lors de la suppression');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredFavorites = favorites.filter(f =>
|
||||||
|
f.business.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
f.business.category.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-20">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-brand-600" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
|
<h2 className="text-2xl font-bold font-serif text-gray-900">Mes Favoris</h2>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Rechercher..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-10 pr-4 py-2 bg-white border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none w-full sm:w-64"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredFavorites.length === 0 ? (
|
||||||
|
<div className="bg-white rounded-xl border border-dashed border-gray-300 p-12 text-center">
|
||||||
|
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Heart className="w-8 h-8 text-gray-200" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 mb-1">Aucun favori</h3>
|
||||||
|
<p className="text-gray-500 max-w-sm mx-auto mb-6">
|
||||||
|
Vous n'avez pas encore ajouté d'entrepreneur à vos favoris. Parcourez l'annuaire pour découvrir des pépites !
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/annuaire"
|
||||||
|
className="inline-flex items-center px-4 py-2 bg-brand-600 text-white font-bold rounded-lg hover:bg-brand-700 transition-colors"
|
||||||
|
>
|
||||||
|
Explorer l'annuaire
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{filteredFavorites.map((fav) => (
|
||||||
|
<div key={fav.id} className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden group flex flex-col h-full relative">
|
||||||
|
<div className="relative h-32 bg-gray-100">
|
||||||
|
<img
|
||||||
|
src={fav.business.logoUrl || `https://picsum.photos/seed/${fav.business.id}/400/200`}
|
||||||
|
alt={fav.business.name}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-colors" />
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleRemoveFavorite(e, fav.business.id)}
|
||||||
|
className="absolute top-2 right-2 p-2 bg-white rounded-full text-red-500 shadow-sm hover:scale-110 transition-transform"
|
||||||
|
title="Retirer des favoris"
|
||||||
|
>
|
||||||
|
<Heart className="w-4 h-4 fill-current" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 flex-1 flex flex-col">
|
||||||
|
<h4 className="font-bold text-gray-900 truncate mb-1">{fav.business.name}</h4>
|
||||||
|
<p className="text-[10px] text-brand-600 font-bold uppercase tracking-wider mb-2">{fav.business.category}</p>
|
||||||
|
|
||||||
|
<div className="flex items-center text-xs text-gray-500 mb-2">
|
||||||
|
<MapPin className="w-3.5 h-3.5 mr-1" />
|
||||||
|
{fav.business.location}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 mt-auto pt-3 border-t border-gray-50">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Star className="w-3 h-3 text-yellow-400 fill-current" />
|
||||||
|
<span className="text-[11px] font-bold ml-1">{fav.business.rating}</span>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href={`/annuaire/${fav.business.slug || fav.business.id}`}
|
||||||
|
className="ml-auto text-[11px] font-bold text-brand-600 flex items-center gap-1 hover:underline"
|
||||||
|
>
|
||||||
|
Voir la fiche
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardFavorites;
|
||||||
@@ -112,7 +112,7 @@ const DashboardOverview = ({ business, stats }: {
|
|||||||
<div className="px-2 py-1 bg-brand-50 text-brand-700 text-[10px] font-bold uppercase rounded leading-none">30 derniers jours</div>
|
<div className="px-2 py-1 bg-brand-50 text-brand-700 text-[10px] font-bold uppercase rounded leading-none">30 derniers jours</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-72 w-full">
|
<div className="h-72 w-full">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%" minWidth={0}>
|
||||||
<AreaChart data={stats?.dailyStats || []}>
|
<AreaChart data={stats?.dailyStats || []}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="colorViews" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="colorViews" x1="0" y1="0" x2="0" y2="1">
|
||||||
@@ -154,7 +154,7 @@ const DashboardOverview = ({ business, stats }: {
|
|||||||
<div className="px-2 py-1 bg-blue-50 text-blue-700 text-[10px] font-bold uppercase rounded leading-none">30 derniers jours</div>
|
<div className="px-2 py-1 bg-blue-50 text-blue-700 text-[10px] font-bold uppercase rounded leading-none">30 derniers jours</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-72 w-full">
|
<div className="h-72 w-full">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%" minWidth={0}>
|
||||||
<AreaChart data={stats?.dailyStats || []}>
|
<AreaChart data={stats?.dailyStats || []}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="colorClicks" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="colorClicks" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
|||||||
35
features.md
Normal file
35
features.md
Normal file
@@ -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.
|
||||||
|
|
||||||
@@ -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} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
|
turbopack: {
|
||||||
|
root: __dirname,
|
||||||
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ model User {
|
|||||||
reports MessageReport[]
|
reports MessageReport[]
|
||||||
ratings Rating[]
|
ratings Rating[]
|
||||||
country Country? @relation(fields: [countryId], references: [id])
|
country Country? @relation(fields: [countryId], references: [id])
|
||||||
|
favorites Favorite[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Business {
|
model Business {
|
||||||
@@ -82,6 +83,7 @@ model Business {
|
|||||||
conversations Conversation[]
|
conversations Conversation[]
|
||||||
offers Offer[]
|
offers Offer[]
|
||||||
ratings Rating[]
|
ratings Rating[]
|
||||||
|
favorites Favorite[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model BusinessCategory {
|
model BusinessCategory {
|
||||||
@@ -317,6 +319,17 @@ model Event {
|
|||||||
tags String[] @default([])
|
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 {
|
enum RatingStatus {
|
||||||
PENDING
|
PENDING
|
||||||
APPROVED
|
APPROVED
|
||||||
|
|||||||
Reference in New Issue
Block a user