343 lines
23 KiB
TypeScript
343 lines
23 KiB
TypeScript
"use client";
|
|
|
|
|
|
import React, { useState } from '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';
|
|
import { useUser } from '../UserProvider';
|
|
|
|
// 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 { login } = useUser();
|
|
const [formData, setFormData] = useState(business);
|
|
const [videoInput, setVideoInput] = useState(business.videoUrl || '');
|
|
const [videoPreviewId, setVideoPreviewId] = useState<string | null>(getYouTubeId(business.videoUrl || ''));
|
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
const [isMounted, setIsMounted] = useState(false);
|
|
|
|
// Sync formData when business prop changes (initial fetch)
|
|
React.useEffect(() => {
|
|
setFormData(business);
|
|
setVideoInput(business.videoUrl || '');
|
|
setVideoPreviewId(getYouTubeId(business.videoUrl || ''));
|
|
}, [business]);
|
|
|
|
React.useEffect(() => {
|
|
setIsMounted(true);
|
|
}, []);
|
|
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
|
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<HTMLInputElement>) => {
|
|
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 = async () => {
|
|
try {
|
|
const res = await fetch('/api/businesses', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'x-user-id': business?.ownerId || (typeof window !== 'undefined' ? JSON.parse(localStorage.getItem('afro_user') || '{}').id : '')
|
|
},
|
|
body: JSON.stringify({
|
|
...formData,
|
|
ownerId: business?.ownerId || (typeof window !== 'undefined' ? JSON.parse(localStorage.getItem('afro_user') || '{}').id : null)
|
|
})
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
if (data.error) {
|
|
toast.error(data.error);
|
|
} else {
|
|
setBusiness(data);
|
|
if (data.owner && login) {
|
|
login(data.owner);
|
|
}
|
|
toast.success('Profil mis à jour avec succès !');
|
|
// Optional: refresh page to show in annuaire
|
|
}
|
|
} catch (error) {
|
|
console.error('Save error:', error);
|
|
toast.error('Erreur lors de la sauvegarde');
|
|
}
|
|
};
|
|
|
|
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 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>
|
|
<div className="flex items-center space-x-6">
|
|
<div className="shrink-0">
|
|
<img className="h-24 w-24 object-cover rounded-full border-2 border-gray-200" src={formData.logoUrl} alt="Logo actuel" />
|
|
</div>
|
|
<div className="flex-1 border-2 border-dashed border-gray-300 rounded-md p-6 flex flex-col items-center justify-center hover:border-brand-400 transition-colors cursor-pointer bg-gray-50">
|
|
<ImageIcon className="h-8 w-8 text-gray-400" />
|
|
<p className="mt-1 text-xs text-gray-500">Glissez votre logo ici ou cliquez pour parcourir</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6 mt-6">
|
|
<div className="sm:col-span-3">
|
|
<label className="block text-sm font-medium text-gray-700">Nom de l'entreprise</label>
|
|
<input type="text" name="name" value={formData.name} 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>
|
|
<div className="sm:col-span-3">
|
|
<label className="block text-sm font-medium text-gray-700">Lien personnalisé (URL)</label>
|
|
<div className="mt-1 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-xs">
|
|
/annuaire/
|
|
</span>
|
|
<input
|
|
type="text"
|
|
name="slug"
|
|
placeholder="mon-entreprise"
|
|
value={formData.slug || ''}
|
|
onChange={(e) => {
|
|
const val = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
setFormData({ ...formData, slug: val });
|
|
}}
|
|
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>
|
|
<p className="mt-1 text-xs text-gray-500">Uniquement lettres, chiffres et tirets. Laissez vide pour utiliser l'ID par défaut.</p>
|
|
</div>
|
|
<div className="sm:col-span-3">
|
|
<label className="block text-sm font-medium text-gray-700">Secteur d'activité</label>
|
|
<select name="category" value={formData.category} 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">
|
|
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
|
|
</select>
|
|
</div>
|
|
<div className="sm:col-span-6">
|
|
<div className="flex justify-between mb-1">
|
|
<label className="block text-sm font-medium text-gray-700">Description</label>
|
|
<button type="button" onClick={handleAiGenerate} disabled={isGenerating} className="text-xs flex items-center text-brand-600 hover:text-brand-800">
|
|
{isGenerating ? '...' : <><Sparkles className="w-3 h-3 mr-1" /> Générer avec IA</>}
|
|
</button>
|
|
</div>
|
|
<textarea name="description" rows={3} value={formData.description} 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" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* B. Bloc Présentation Vidéo */}
|
|
<div className="bg-white shadow rounded-lg p-6">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center"><Youtube className="w-5 h-5 mr-2 text-red-600" /> Présentation Vidéo</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Lien de votre vidéo (Youtube)</label>
|
|
<div className="mt-1 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 sm:text-sm">
|
|
https://
|
|
</span>
|
|
<input
|
|
type="text"
|
|
value={videoInput.replace('https://', '')}
|
|
onChange={handleVideoChange}
|
|
className="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md focus:ring-brand-500 focus:border-brand-500 sm:text-sm border-gray-300"
|
|
placeholder="www.youtube.com/watch?v=..."
|
|
/>
|
|
</div>
|
|
<p className="mt-2 text-sm text-gray-500">Copiez l'URL de votre vidéo de présentation pour l'afficher sur votre profil.</p>
|
|
</div>
|
|
|
|
{videoPreviewId ? (
|
|
<div className="aspect-w-16 aspect-h-9 rounded-lg overflow-hidden bg-gray-100 mt-4 border border-gray-200">
|
|
<iframe
|
|
src={`https://www.youtube.com/embed/${videoPreviewId}`}
|
|
title="YouTube video player"
|
|
frameBorder="0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
allowFullScreen
|
|
className="w-full h-64 sm:h-80 rounded-lg"
|
|
></iframe>
|
|
</div>
|
|
) : videoInput && (
|
|
<div className="rounded-md bg-red-50 p-4 mt-4">
|
|
<div className="flex">
|
|
<div className="flex-shrink-0">
|
|
<X className="h-5 w-5 text-red-400" aria-hidden="true" />
|
|
</div>
|
|
<div className="ml-3">
|
|
<h3 className="text-sm font-medium text-red-800">Lien invalide</h3>
|
|
<div className="mt-2 text-sm text-red-700">
|
|
<p>Impossible de détecter une vidéo YouTube valide. Vérifiez le lien.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* C. Bloc Coordonnées & Réseaux */}
|
|
<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-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">
|
|
<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">
|
|
<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">
|
|
<Facebook className="w-4 h-4" />
|
|
</span>
|
|
<input type="text" placeholder="Lien Facebook" value={formData.socialLinks?.facebook || ''} onChange={(e) => handleSocialChange('facebook', e.target.value)} className="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md focus:ring-brand-500 focus:border-brand-500 sm:text-sm border-gray-300" />
|
|
</div>
|
|
<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">
|
|
<Linkedin className="w-4 h-4" />
|
|
</span>
|
|
<input type="text" placeholder="Lien LinkedIn" value={formData.socialLinks?.linkedin || ''} onChange={(e) => handleSocialChange('linkedin', e.target.value)} className="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md focus:ring-brand-500 focus:border-brand-500 sm:text-sm border-gray-300" />
|
|
</div>
|
|
<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">
|
|
<Instagram className="w-4 h-4" />
|
|
</span>
|
|
<input type="text" placeholder="Lien Instagram" value={formData.socialLinks?.instagram || ''} onChange={(e) => handleSocialChange('instagram', e.target.value)} className="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md focus:ring-brand-500 focus:border-brand-500 sm:text-sm border-gray-300" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DashboardProfile;
|