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:
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Image as ImageIcon, Sparkles, Youtube, X, Globe, Facebook, Linkedin, Instagram } from 'lucide-react';
|
||||
import { Image as ImageIcon, Sparkles, Youtube, X, Globe, Facebook, Linkedin, Instagram, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import { Business, CATEGORIES } from '../../types';
|
||||
import { generateBusinessDescription } from '../../lib/geminiService';
|
||||
import { toast } from 'react-hot-toast';
|
||||
@@ -86,15 +86,60 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
|
||||
}
|
||||
};
|
||||
|
||||
const isNameOk = formData.name && formData.name !== "Nouvelle Entreprise";
|
||||
const isDescOk = formData.description && formData.description.length >= 20;
|
||||
const isLocOk = formData.location && formData.location !== "Ma Ville";
|
||||
const isEmailOk = !!formData.contactEmail;
|
||||
const isPhoneOk = !!formData.contactPhone;
|
||||
const isActive = isNameOk && isDescOk && isLocOk && isEmailOk && isPhoneOk;
|
||||
|
||||
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
|
||||
<div className="space-y-8 pb-20">
|
||||
<div className="flex justify-between items-center bg-white p-4 rounded-lg shadow-sm sticky top-0 z-10 border border-gray-100">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="text-xl font-bold font-serif text-gray-900">Éditer mon profil</h2>
|
||||
{isActive ? (
|
||||
<span className="px-3 py-1 bg-green-100 text-green-700 text-xs font-bold rounded-full flex items-center gap-1">
|
||||
<CheckCircle className="w-3 h-3" /> Boutique Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-3 py-1 bg-orange-100 text-orange-700 text-xs font-bold rounded-full flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" /> Profil Incomplet
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button onClick={handleSave} className="bg-brand-600 text-white px-6 py-2 rounded-lg hover:bg-brand-700 font-bold text-sm shadow-md transition-all active:scale-95">
|
||||
Enregistrer les modifications
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!isActive && (
|
||||
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4 flex gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-orange-600 shrink-0" />
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-orange-800">Votre boutique n'est pas encore visible dans l'annuaire</h4>
|
||||
<p className="text-xs text-orange-700 mt-1">Pour être activé, vous devez compléter :</p>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 mt-2">
|
||||
<span className={`text-[10px] flex items-center gap-1 ${isNameOk ? 'text-green-600' : 'text-orange-400'}`}>
|
||||
{isNameOk ? '✓' : '○'} Nom de boutique
|
||||
</span>
|
||||
<span className={`text-[10px] flex items-center gap-1 ${isDescOk ? 'text-green-600' : 'text-orange-400'}`}>
|
||||
{isDescOk ? '✓' : '○'} Description (min 20 chars)
|
||||
</span>
|
||||
<span className={`text-[10px] flex items-center gap-1 ${isLocOk ? 'text-green-600' : 'text-orange-400'}`}>
|
||||
{isLocOk ? '✓' : '○'} Localisation
|
||||
</span>
|
||||
<span className={`text-[10px] flex items-center gap-1 ${isEmailOk ? 'text-green-600' : 'text-orange-400'}`}>
|
||||
{isEmailOk ? '✓' : '○'} Email de contact
|
||||
</span>
|
||||
<span className={`text-[10px] flex items-center gap-1 ${isPhoneOk ? 'text-green-600' : 'text-orange-400'}`}>
|
||||
{isPhoneOk ? '✓' : '○'} Numéro de téléphone
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
@@ -204,16 +249,64 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
|
||||
<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 className="sm:col-span-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Localisation (Ville, Pays)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="location"
|
||||
value={formData.location}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Ex: Abidjan, Côte d'Ivoire"
|
||||
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 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 className="flex items-center justify-between mb-1">
|
||||
<label className="block text-sm font-medium text-gray-700">Email Contact</label>
|
||||
<label className="relative inline-flex items-center cursor-pointer scale-75">
|
||||
<input type="checkbox" className="sr-only peer" checked={formData.showEmail} onChange={(e) => setFormData({...formData, showEmail: e.target.checked})} />
|
||||
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-brand-600"></div>
|
||||
<span className="ml-2 text-[10px] font-medium text-gray-500">{formData.showEmail ? 'Public' : 'Masqué'}</span>
|
||||
</label>
|
||||
</div>
|
||||
<input type="email" name="contactEmail" value={formData.contactEmail} 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" placeholder="contact@votre-boutique.com" />
|
||||
</div>
|
||||
<div className="sm:col-span-3">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="block text-sm font-medium text-gray-700">Téléphone</label>
|
||||
<label className="relative inline-flex items-center cursor-pointer scale-75">
|
||||
<input type="checkbox" className="sr-only peer" checked={formData.showPhone} onChange={(e) => setFormData({...formData, showPhone: e.target.checked})} />
|
||||
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-brand-600"></div>
|
||||
<span className="ml-2 text-[10px] font-medium text-gray-500">{formData.showPhone ? 'Public' : 'Masqué'}</span>
|
||||
</label>
|
||||
</div>
|
||||
<input type="text" name="contactPhone" value={formData.contactPhone || ''} 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" placeholder="+225 ..." />
|
||||
</div>
|
||||
<div className="sm:col-span-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Lien du Site Internet</label>
|
||||
<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 text-sm">
|
||||
https://
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
name="websiteUrl"
|
||||
placeholder="www.votre-site.com"
|
||||
value={(formData.websiteUrl || '').replace('https://', '')}
|
||||
onChange={(e) => setFormData({...formData, websiteUrl: `https://${e.target.value}`})}
|
||||
className="flex-1 block w-full border border-gray-300 rounded-none rounded-r-md py-2 px-3 focus:ring-brand-500 focus:border-brand-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</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="flex justify-between items-center mb-3">
|
||||
<h4 className="text-sm font-medium text-gray-500 uppercase">Réseaux Sociaux</h4>
|
||||
<label className="relative inline-flex items-center cursor-pointer scale-75">
|
||||
<input type="checkbox" className="sr-only peer" checked={formData.showSocials} onChange={(e) => setFormData({...formData, showSocials: e.target.checked})} />
|
||||
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-brand-600"></div>
|
||||
<span className="ml-2 text-[10px] font-medium text-gray-500">{formData.showSocials ? 'Public' : 'Masqué'}</span>
|
||||
</label>
|
||||
</div>
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user