synchronisation du contenu avec la DB et amélioration du CMS
- Migration du blog et des interviews des données mockées vers PostgreSQL (Prisma). - Refonte des pages publiques en Server Components pour de meilleures performances/SEO. - Intégration d'un éditeur de texte riche (React Quill) avec titres H1-H6 et séparateurs. - Correction des erreurs de validation Prisma liées aux paramètres asynchrones (Next.js 15+). - Correction des problèmes d'affichage des modales de suspension via React Portals. - Installation de @tailwindcss/typography et correction du débordement de texte (break-words). - Implémentation des actions de modération (suspension/révocation) pour les utilisateurs et boutiques.
This commit is contained in:
@@ -3,16 +3,20 @@
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { ArrowLeft, MapPin, Globe, Mail, Phone, CheckCircle, Star, Facebook, Linkedin, Instagram, Play, Share2, Send, Award, Quote } from 'lucide-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, MapPin, Globe, Mail, Phone, CheckCircle, Star, Facebook, Linkedin, Instagram, Play, Share2, Send, Award, Quote, MessageCircle, Lock, Loader2 } from 'lucide-react';
|
||||
import { MOCK_BUSINESSES, MOCK_OFFERS } from '../../../lib/mockData';
|
||||
import { Business, OfferType } from '../../../types';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useUser } from '../../../components/UserProvider';
|
||||
|
||||
const BusinessDetailPage = () => {
|
||||
const { id } = useParams();
|
||||
const router = useRouter();
|
||||
const { user } = useUser();
|
||||
const [business, setBusiness] = useState<Business | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [existingConversationId, setExistingConversationId] = useState<string | null>(null);
|
||||
|
||||
const [contactForm, setContactForm] = useState({
|
||||
name: '',
|
||||
@@ -50,7 +54,28 @@ const BusinessDetailPage = () => {
|
||||
const data = await res.json();
|
||||
setBusiness(data);
|
||||
|
||||
// 3. Increment views silently
|
||||
// 3. Track view event
|
||||
// Only track if not owner
|
||||
const currentUserStr = localStorage.getItem('afro_user');
|
||||
const currentUser = currentUserStr ? JSON.parse(currentUserStr) : null;
|
||||
if (!currentUser || currentUser.id !== data.ownerId) {
|
||||
fetch('/api/analytics', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: 'BUSINESS_VIEW',
|
||||
path: window.location.pathname,
|
||||
label: data.name,
|
||||
metadata: {
|
||||
businessId: data.id,
|
||||
businessName: data.name,
|
||||
userId: currentUser?.id || 'anonymous'
|
||||
}
|
||||
})
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
// Also increment views count in DB (Legacy support)
|
||||
fetch(`/api/businesses/${id}/view`, { method: 'POST' }).catch(console.error);
|
||||
} else {
|
||||
setBusiness(null);
|
||||
@@ -66,6 +91,26 @@ const BusinessDetailPage = () => {
|
||||
if (id) loadBusiness();
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkExistingConversation = async () => {
|
||||
if (!user || user.id === 'undefined' || !business?.id) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/conversations?businessId=${business.id}`, {
|
||||
headers: { 'x-user-id': user.id }
|
||||
});
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
setExistingConversationId(data[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking existing conversation:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (user && business) checkExistingConversation();
|
||||
}, [user, business]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-[60vh] flex flex-col items-center justify-center">
|
||||
@@ -95,18 +140,77 @@ const BusinessDetailPage = () => {
|
||||
|
||||
const videoEmbedUrl = business.videoUrl ? getEmbedUrl(business.videoUrl) : null;
|
||||
|
||||
const handleContactSubmit = (e: React.FormEvent) => {
|
||||
const handleTrackEvent = async (type: string, label?: string) => {
|
||||
if (!business?.id) return;
|
||||
|
||||
// Don't track if the current user is the owner
|
||||
if (user && user.id === business.ownerId) return;
|
||||
|
||||
try {
|
||||
fetch('/api/analytics', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type,
|
||||
path: window.location.pathname,
|
||||
label: label || business.name,
|
||||
metadata: {
|
||||
businessId: business.id,
|
||||
businessName: business.name,
|
||||
userId: user?.id || 'anonymous'
|
||||
}
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Analytics error:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContactSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!user) {
|
||||
toast.error('Veuillez vous connecter pour envoyer un message');
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
setFormStatus('sending');
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setFormStatus('success');
|
||||
setContactForm({ name: '', email: '', message: '' });
|
||||
try {
|
||||
const res = await fetch('/api/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-user-id': user.id
|
||||
},
|
||||
body: JSON.stringify({
|
||||
businessId: business.id,
|
||||
content: contactForm.message
|
||||
})
|
||||
});
|
||||
|
||||
// Reset status after 3 seconds
|
||||
setTimeout(() => setFormStatus('idle'), 3000);
|
||||
}, 1500);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.error) {
|
||||
// Track contact event
|
||||
handleTrackEvent('BUSINESS_CONTACT_CLICK', 'Message Sent');
|
||||
|
||||
setFormStatus('success');
|
||||
toast.success('Message envoyé ! Redirection vers votre messagerie...');
|
||||
setContactForm({ ...contactForm, message: '' });
|
||||
|
||||
setTimeout(() => {
|
||||
router.push('/dashboard?view=messages'); // Hypothetical view param handle later
|
||||
}, 2000);
|
||||
} else {
|
||||
toast.error(data.error);
|
||||
setFormStatus('idle');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Erreur lors de l'envoi");
|
||||
setFormStatus('idle');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -276,24 +380,45 @@ const BusinessDetailPage = () => {
|
||||
<div id="contact-form" className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-24">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">Contact</h3>
|
||||
|
||||
{/* Direct Info */}
|
||||
<div className="space-y-4 mb-6">
|
||||
<a href={`mailto:${business.contactEmail}`} className="flex items-center text-gray-600 hover:text-brand-600 transition-colors">
|
||||
<div className="flex items-center text-gray-600">
|
||||
<div className="w-8 h-8 rounded-full bg-brand-50 flex items-center justify-center mr-3 text-brand-600 flex-shrink-0">
|
||||
<Mail className="w-4 h-4" />
|
||||
<MapPin className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="text-sm truncate">{business.contactEmail}</span>
|
||||
</a>
|
||||
{business.contactPhone && (
|
||||
<a href={`tel:${business.contactPhone}`} className="flex items-center text-gray-600 hover:text-brand-600 transition-colors">
|
||||
<span className="text-sm">{business.location}</span>
|
||||
</div>
|
||||
{business.showEmail && (
|
||||
<a
|
||||
href={`mailto:${business.contactEmail}`}
|
||||
onClick={() => handleTrackEvent('BUSINESS_CONTACT_CLICK', 'Email')}
|
||||
className="flex items-center text-gray-600 hover:text-brand-600 transition-colors"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-brand-50 flex items-center justify-center mr-3 text-brand-600 flex-shrink-0">
|
||||
<Mail className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="text-sm truncate">{business.contactEmail}</span>
|
||||
</a>
|
||||
)}
|
||||
{(business.contactPhone && business.showPhone) && (
|
||||
<a
|
||||
href={`tel:${business.contactPhone}`}
|
||||
onClick={() => handleTrackEvent('BUSINESS_CONTACT_CLICK', 'Phone')}
|
||||
className="flex items-center text-gray-600 hover:text-brand-600 transition-colors"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-brand-50 flex items-center justify-center mr-3 text-brand-600 flex-shrink-0">
|
||||
<Phone className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="text-sm">{business.contactPhone}</span>
|
||||
</a>
|
||||
)}
|
||||
{business.socialLinks?.website && (
|
||||
<a href={business.socialLinks.website} target="_blank" rel="noopener noreferrer" className="flex items-center text-gray-600 hover:text-brand-600 transition-colors">
|
||||
{(business.websiteUrl || business.socialLinks?.website) && (
|
||||
<a
|
||||
href={business.websiteUrl || business.socialLinks?.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => handleTrackEvent('BUSINESS_CONTACT_CLICK', 'Website')}
|
||||
className="flex items-center text-gray-600 hover:text-brand-600 transition-colors"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-brand-50 flex items-center justify-center mr-3 text-brand-600 flex-shrink-0">
|
||||
<Globe className="w-4 h-4" />
|
||||
</div>
|
||||
@@ -302,72 +427,95 @@ const BusinessDetailPage = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contact Form */}
|
||||
{/* Contact Section */}
|
||||
<div className="border-t border-gray-100 pt-6">
|
||||
<h4 className="text-sm font-semibold text-gray-900 mb-3">Envoyer un message</h4>
|
||||
{formStatus === 'success' ? (
|
||||
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-md text-sm">
|
||||
Message envoyé avec succès !
|
||||
<h4 className="text-sm font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<MessageCircle className="w-4 h-4 text-brand-600" />
|
||||
Discuter avec l'entrepreneur
|
||||
</h4>
|
||||
|
||||
{existingConversationId ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-500 text-center bg-brand-50 p-4 rounded-xl border border-brand-100">
|
||||
Vous avez déjà une discussion en cours avec cet entrepreneur.
|
||||
</p>
|
||||
<Link
|
||||
href={`/dashboard?view=messages&chatId=${existingConversationId}`}
|
||||
className="w-full flex justify-center items-center gap-2 px-4 py-3 border border-transparent text-sm font-bold rounded-xl text-white bg-brand-600 hover:bg-brand-700 shadow-lg transition-all transform active:scale-[0.98]"
|
||||
>
|
||||
<MessageCircle className="w-5 h-5" />
|
||||
Continuer la discussion
|
||||
</Link>
|
||||
</div>
|
||||
) : formStatus === 'success' ? (
|
||||
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-8 rounded-xl text-center">
|
||||
<CheckCircle className="w-10 h-10 mx-auto mb-2 text-green-500" />
|
||||
<h4 className="font-bold">Message envoyé !</h4>
|
||||
<p className="text-sm">Redirection vers votre messagerie...</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleContactSubmit} className="space-y-3">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Votre nom"
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"
|
||||
value={contactForm.name}
|
||||
onChange={(e) => setContactForm({ ...contactForm, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Votre email"
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"
|
||||
value={contactForm.email}
|
||||
onChange={(e) => setContactForm({ ...contactForm, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<form onSubmit={handleContactSubmit} className="space-y-4">
|
||||
<div className="relative">
|
||||
<textarea
|
||||
placeholder="Votre message..."
|
||||
placeholder={user ? "Bonjour, j'aimerais en savoir plus sur..." : "Connectez-vous pour envoyer un message"}
|
||||
required
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-brand-500 resize-none"
|
||||
rows={4}
|
||||
disabled={!user || formStatus === 'sending'}
|
||||
className="w-full px-4 py-3 bg-gray-50 border border-transparent rounded-xl text-sm focus:bg-white focus:ring-2 focus:ring-brand-500 outline-none transition-all resize-none disabled:opacity-60 shadow-inner"
|
||||
value={contactForm.message}
|
||||
onChange={(e) => setContactForm({ ...contactForm, message: e.target.value })}
|
||||
/>
|
||||
{!user && (
|
||||
<div className="absolute inset-0 bg-white/20 backdrop-blur-[1px] rounded-xl flex items-center justify-center">
|
||||
<Lock className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={formStatus === 'sending'}
|
||||
className="w-full flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-gray-900 hover:bg-gray-800 focus:outline-none transition-colors disabled:opacity-70"
|
||||
disabled={!user || formStatus === 'sending' || !contactForm.message.trim()}
|
||||
className="w-full flex justify-center items-center gap-2 px-4 py-3 border border-transparent text-sm font-bold rounded-xl text-white bg-brand-600 hover:bg-brand-700 shadow-lg transition-all disabled:opacity-50 disabled:grayscale transform active:scale-[0.98]"
|
||||
>
|
||||
{formStatus === 'sending' ? 'Envoi...' : <><Send className="w-3 h-3 mr-2" /> Envoyer</>}
|
||||
{formStatus === 'sending' ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin text-white" />
|
||||
) : (
|
||||
<>
|
||||
{user ? <Send className="w-4 h-4" /> : <Lock className="w-4 h-4" />}
|
||||
{user ? "Lancer la discussion" : "Action réservée aux membres"}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{!user && (
|
||||
<p className="text-[10px] text-center text-gray-400">
|
||||
Inscrivez-vous gratuitement pour contacter les entrepreneurs.
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-gray-100">
|
||||
<div className="flex justify-center space-x-6">
|
||||
{business.socialLinks?.facebook && (
|
||||
<a href={business.socialLinks.facebook} target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-[#1877F2] transition-colors">
|
||||
<Facebook className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
{business.socialLinks?.linkedin && (
|
||||
<a href={business.socialLinks.linkedin} target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-[#0A66C2] transition-colors">
|
||||
<Linkedin className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
{business.socialLinks?.instagram && (
|
||||
<a href={business.socialLinks.instagram} target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-[#E4405F] transition-colors">
|
||||
<Instagram className="w-5 h-5" />
|
||||
</a>
|
||||
{business.showSocials && (
|
||||
<>
|
||||
{business.socialLinks?.facebook && (
|
||||
<a href={business.socialLinks.facebook} target="_blank" rel="noopener noreferrer" onClick={() => handleTrackEvent('BUSINESS_CONTACT_CLICK', 'Facebook')} className="text-gray-400 hover:text-[#1877F2] transition-colors">
|
||||
<Facebook className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
{business.socialLinks?.linkedin && (
|
||||
<a href={business.socialLinks.linkedin} target="_blank" rel="noopener noreferrer" onClick={() => handleTrackEvent('BUSINESS_CONTACT_CLICK', 'LinkedIn')} className="text-gray-400 hover:text-[#0A66C2] transition-colors">
|
||||
<Linkedin className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
{business.socialLinks?.instagram && (
|
||||
<a href={business.socialLinks.instagram} target="_blank" rel="noopener noreferrer" onClick={() => handleTrackEvent('BUSINESS_CONTACT_CLICK', 'Instagram')} className="text-gray-400 hover:text-[#E4405F] transition-colors">
|
||||
<Instagram className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user