756 lines
45 KiB
TypeScript
756 lines
45 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Image as ImageIcon, Sparkles, Youtube, X, Globe, Facebook, Linkedin, Instagram, CheckCircle, AlertCircle, Hash } from 'lucide-react';
|
|
import { Business, Country } from '../../types';
|
|
import TagInput from '../TagInput';
|
|
import { generateBusinessDescription } from '../../lib/geminiService';
|
|
import { toast } from 'react-hot-toast';
|
|
import { useUser } from '../UserProvider';
|
|
import { uploadImage } from '../../app/actions/upload';
|
|
import { Loader2, Upload, Link as LinkIcon } from 'lucide-react';
|
|
|
|
// 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 normalizeBusinessData = (b: Business): Business => ({
|
|
...b,
|
|
name: b.name || 'Nouvelle Entreprise',
|
|
category: b.category || 'Autre',
|
|
categoryId: b.categoryId || '',
|
|
suggestedCategory: b.suggestedCategory || '',
|
|
description: b.description || '',
|
|
location: b.location || '',
|
|
countryId: b.countryId || '',
|
|
city: b.city || '',
|
|
contactEmail: b.contactEmail || '',
|
|
showEmail: b.showEmail ?? true,
|
|
showPhone: b.showPhone ?? true,
|
|
showSocials: b.showSocials ?? true,
|
|
slug: b.slug || '',
|
|
videoUrl: b.videoUrl || '',
|
|
coverUrl: b.coverUrl || '',
|
|
coverPosition: b.coverPosition || '50% 50%',
|
|
coverZoom: b.coverZoom || 1,
|
|
contactPhone: b.contactPhone || '',
|
|
websiteUrl: b.websiteUrl || '',
|
|
socialLinks: {
|
|
facebook: b.socialLinks?.facebook || '',
|
|
linkedin: b.socialLinks?.linkedin || '',
|
|
instagram: b.socialLinks?.instagram || '',
|
|
website: b.socialLinks?.website || '',
|
|
}
|
|
});
|
|
|
|
const DashboardProfile = ({ business, setBusiness }: { business: Business, setBusiness: (b: Business) => void }) => {
|
|
const { login } = useUser();
|
|
const [formData, setFormData] = useState<Business>(normalizeBusinessData(business));
|
|
const [countries, setCountries] = useState<Country[]>([]);
|
|
const [categories, setCategories] = useState<any[]>([]);
|
|
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);
|
|
const [allTags, setAllTags] = useState<string[]>([]);
|
|
const [isUploadingLogo, setIsUploadingLogo] = useState(false);
|
|
const [isUploadingCover, setIsUploadingCover] = useState(false);
|
|
const [logoInputMode, setLogoInputMode] = useState<'url' | 'upload'>('upload');
|
|
const [coverInputMode, setCoverInputMode] = useState<'url' | 'upload'>('upload');
|
|
const logoFileInputRef = React.useRef<HTMLInputElement>(null);
|
|
const coverFileInputRef = React.useRef<HTMLInputElement>(null);
|
|
// Fetch countries, categories and tags
|
|
React.useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
const [cRes, catRes, tagsRes] = await Promise.all([
|
|
fetch('/api/countries'),
|
|
fetch('/api/categories'),
|
|
fetch('/api/tags')
|
|
]);
|
|
const cData = await cRes.json();
|
|
const catData = await catRes.json();
|
|
const tagsData = await tagsRes.json();
|
|
if (!cData.error) setCountries(cData);
|
|
if (!catData.error) setCategories(catData);
|
|
if (!tagsData.error) setAllTags(tagsData);
|
|
} catch (error) {
|
|
console.error('Error fetching metadata:', error);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, []);
|
|
|
|
// Sync formData when business prop changes (initial fetch)
|
|
React.useEffect(() => {
|
|
setFormData(normalizeBusinessData(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 handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
if (!file.type.startsWith('image/')) {
|
|
toast.error("Le fichier doit être une image");
|
|
return;
|
|
}
|
|
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
toast.error("L'image ne doit pas dépasser 5 Mo");
|
|
return;
|
|
}
|
|
|
|
setIsUploadingLogo(true);
|
|
const uploadFormData = new FormData();
|
|
uploadFormData.append('file', file);
|
|
|
|
try {
|
|
const result = await uploadImage(uploadFormData);
|
|
if (result.success && result.url) {
|
|
setFormData({ ...formData, logoUrl: result.url });
|
|
toast.success("Logo mis à jour !");
|
|
} else {
|
|
toast.error(result.error || "Erreur lors de l'upload");
|
|
}
|
|
} catch (error) {
|
|
toast.error("Erreur réseau lors de l'upload");
|
|
} finally {
|
|
setIsUploadingLogo(false);
|
|
if (logoFileInputRef.current) logoFileInputRef.current.value = '';
|
|
}
|
|
};
|
|
|
|
const handleCoverUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
if (!file.type.startsWith('image/')) {
|
|
toast.error("Le fichier doit être une image");
|
|
return;
|
|
}
|
|
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
toast.error("L'image ne doit pas dépasser 5 Mo");
|
|
return;
|
|
}
|
|
|
|
setIsUploadingCover(true);
|
|
const uploadFormData = new FormData();
|
|
uploadFormData.append('file', file);
|
|
|
|
try {
|
|
const result = await uploadImage(uploadFormData);
|
|
if (result.success && result.url) {
|
|
setFormData({ ...formData, coverUrl: result.url });
|
|
toast.success("Bannière mise à jour !");
|
|
} else {
|
|
toast.error(result.error || "Erreur lors de l'upload");
|
|
}
|
|
} catch (error) {
|
|
toast.error("Erreur réseau lors de l'upload");
|
|
} finally {
|
|
setIsUploadingCover(false);
|
|
if (coverFileInputRef.current) coverFileInputRef.current.value = '';
|
|
}
|
|
};
|
|
|
|
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 handleDelete = async () => {
|
|
if (!window.confirm("⚠️ ATTENTION : Êtes-vous sûr de vouloir supprimer définitivement votre boutique ? Cette action supprimera également vos offres, vos messages et vos statistiques. Cette action est irréversible.")) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`/api/businesses/${business.id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'x-user-id': business.ownerId
|
|
}
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
toast.success('Boutique supprimée avec succès.');
|
|
// Upgrade local storage user if role changed
|
|
const localUser = JSON.parse(localStorage.getItem('afro_user') || '{}');
|
|
if (localUser.id === business.ownerId) {
|
|
localUser.role = 'VISITOR';
|
|
localStorage.setItem('afro_user', JSON.stringify(localUser));
|
|
if (login) login(localUser);
|
|
}
|
|
setTimeout(() => window.location.href = '/dashboard', 1500);
|
|
} else {
|
|
toast.error(data.error || 'Erreur lors de la suppression');
|
|
}
|
|
} catch (error) {
|
|
console.error('Delete error:', error);
|
|
toast.error('Erreur réseau lors de la suppression');
|
|
}
|
|
};
|
|
|
|
const isNameOk = formData.name && formData.name !== "Nouvelle Entreprise";
|
|
const isDescOk = formData.description && formData.description.length >= 20;
|
|
const isLocOk = (formData.countryId && formData.city) || (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 flex-col md:flex-row md:items-center gap-6">
|
|
<div className="shrink-0 flex flex-col items-center gap-2">
|
|
<img
|
|
className="h-24 w-24 object-cover rounded-full border-2 border-brand-200 shadow-sm"
|
|
src={formData.logoUrl || "https://picsum.photos/200/200?random=logo"}
|
|
alt="Logo actuel"
|
|
/>
|
|
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Aperçu Logo</span>
|
|
</div>
|
|
|
|
<div className="flex-1 space-y-4">
|
|
<div className="flex bg-gray-100 p-1 rounded-lg w-fit border border-gray-200">
|
|
<button
|
|
onClick={() => setLogoInputMode('upload')}
|
|
className={`px-3 py-1.5 rounded-md text-xs font-bold transition-all flex items-center gap-2 ${logoInputMode === 'upload' ? 'bg-white text-brand-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
|
>
|
|
<Upload className="w-3.5 h-3.5" /> Upload
|
|
</button>
|
|
<button
|
|
onClick={() => setLogoInputMode('url')}
|
|
className={`px-3 py-1.5 rounded-md text-xs font-bold transition-all flex items-center gap-2 ${logoInputMode === 'url' ? 'bg-white text-brand-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
|
>
|
|
<LinkIcon className="w-3.5 h-3.5" /> Lien URL
|
|
</button>
|
|
</div>
|
|
|
|
{logoInputMode === 'upload' ? (
|
|
<div
|
|
onClick={() => logoFileInputRef.current?.click()}
|
|
className="group border-2 border-dashed border-gray-300 rounded-xl p-6 flex flex-col items-center justify-center hover:border-brand-400 hover:bg-brand-50 transition-all cursor-pointer relative overflow-hidden"
|
|
>
|
|
{isUploadingLogo ? (
|
|
<div className="flex flex-col items-center gap-2">
|
|
<Loader2 className="w-8 h-8 text-brand-600 animate-spin" />
|
|
<span className="text-xs font-bold text-brand-600">Upload en cours...</span>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center group-hover:bg-brand-100 group-hover:text-brand-600 transition-colors mb-2">
|
|
<Upload className="w-5 h-5 text-gray-400 group-hover:text-brand-600" />
|
|
</div>
|
|
<p className="text-sm font-bold text-gray-700">Cliquez pour changer le logo</p>
|
|
<p className="text-[10px] text-gray-400 mt-1">PNG, JPG ou WEBP (Max 5Mo)</p>
|
|
</>
|
|
)}
|
|
<input
|
|
type="file"
|
|
ref={logoFileInputRef}
|
|
onChange={handleLogoUpload}
|
|
accept="image/*"
|
|
className="hidden"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="relative">
|
|
<LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
name="logoUrl"
|
|
value={formData.logoUrl || ''}
|
|
onChange={handleInputChange}
|
|
placeholder="URL de votre logo (ex: https://...)"
|
|
className="block w-full border border-gray-300 rounded-lg py-2.5 pl-10 pr-3 focus:ring-brand-500 focus:border-brand-500 sm:text-sm shadow-sm"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<label className="block text-sm font-medium text-gray-700">Photo de Couverture (Bannière)</label>
|
|
<div className="flex bg-gray-100 p-1 rounded-lg border border-gray-200">
|
|
<button
|
|
onClick={() => setCoverInputMode('upload')}
|
|
className={`px-3 py-1 rounded-md text-[10px] font-bold transition-all flex items-center gap-1.5 ${coverInputMode === 'upload' ? 'bg-white text-brand-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
|
>
|
|
<Upload className="w-3 h-3" /> Upload
|
|
</button>
|
|
<button
|
|
onClick={() => setCoverInputMode('url')}
|
|
className={`px-3 py-1 rounded-md text-[10px] font-bold transition-all flex items-center gap-1.5 ${coverInputMode === 'url' ? 'bg-white text-brand-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
|
>
|
|
<LinkIcon className="w-3 h-3" /> URL
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative h-48 w-full rounded-lg overflow-hidden bg-gray-100 border-2 border-dashed border-gray-300 group transition-all">
|
|
{formData.coverUrl ? (
|
|
<img
|
|
src={formData.coverUrl}
|
|
className="w-full h-full object-cover transition-transform duration-200"
|
|
style={{
|
|
objectPosition: formData.coverPosition || '50% 50%',
|
|
transform: `scale(${formData.coverZoom || 1})`
|
|
}}
|
|
alt="Couverture"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex flex-col items-center justify-center text-gray-400">
|
|
<ImageIcon className="w-8 h-8 mb-1" />
|
|
<span className="text-xs">Aucune bannière personnalisée</span>
|
|
</div>
|
|
)}
|
|
|
|
{isUploadingCover && (
|
|
<div className="absolute inset-0 bg-white/60 backdrop-blur-sm flex flex-col items-center justify-center gap-2 z-20">
|
|
<Loader2 className="w-8 h-8 text-brand-600 animate-spin" />
|
|
<span className="text-xs font-bold text-brand-600">Upload en cours...</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center z-10">
|
|
<button
|
|
onClick={() => coverFileInputRef.current?.click()}
|
|
className="bg-white text-gray-900 px-4 py-1.5 rounded-full text-xs font-bold shadow-lg flex items-center gap-2 hover:scale-105 transition-transform"
|
|
>
|
|
<Upload className="w-3.5 h-3.5" /> Changer la photo
|
|
</button>
|
|
</div>
|
|
<input
|
|
type="file"
|
|
ref={coverFileInputRef}
|
|
onChange={handleCoverUpload}
|
|
accept="image/*"
|
|
className="hidden"
|
|
/>
|
|
</div>
|
|
|
|
<div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4 bg-gray-50 p-4 rounded-lg border border-gray-200">
|
|
{/* ... (Zoom/Position controls stay same) */}
|
|
<div>
|
|
<label className="block text-xs font-bold text-gray-500 uppercase mb-2">Zoom : {(formData.coverZoom || 1).toFixed(1)}x</label>
|
|
<input
|
|
type="range"
|
|
min="1" max="3" step="0.1"
|
|
value={formData.coverZoom || 1}
|
|
onChange={(e) => setFormData({...formData, coverZoom: parseFloat(e.target.value)})}
|
|
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-brand-600"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-bold text-gray-500 uppercase mb-2">Position Horizontale</label>
|
|
<input
|
|
type="range"
|
|
min="0" max="100" step="1"
|
|
value={parseInt((formData.coverPosition || '50% 50%').split(' ')[0]) || 50}
|
|
onChange={(e) => {
|
|
const y = (formData.coverPosition || '50% 50%').split(' ')[1] || '50%';
|
|
setFormData({...formData, coverPosition: `${e.target.value}% ${y}`});
|
|
}}
|
|
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-brand-600"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-bold text-gray-500 uppercase mb-2">Position Verticale</label>
|
|
<input
|
|
type="range"
|
|
min="0" max="100" step="1"
|
|
value={parseInt((formData.coverPosition || '50% 50%').split(' ')[1]) || 50}
|
|
onChange={(e) => {
|
|
const x = (formData.coverPosition || '50% 50%').split(' ')[0] || '50%';
|
|
setFormData({...formData, coverPosition: `${x} ${e.target.value}%`});
|
|
}}
|
|
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-brand-600"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{coverInputMode === 'url' && (
|
|
<input
|
|
type="text"
|
|
name="coverUrl"
|
|
value={formData.coverUrl || ''}
|
|
onChange={handleInputChange}
|
|
placeholder="URL de votre image de couverture (ex: https://...)"
|
|
className="mt-4 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 animate-in fade-in slide-in-from-top-1"
|
|
/>
|
|
)}
|
|
<p className="mt-1 text-[10px] text-gray-500 italic">Ajustez le zoom et la position pour un rendu optimal sur votre fiche.</p>
|
|
</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="categoryId"
|
|
value={formData.categoryId || ''}
|
|
onChange={(e) => {
|
|
const catId = e.target.value;
|
|
const catName = categories.find(c => c.id === catId)?.name || 'Autre';
|
|
setFormData({ ...formData, categoryId: catId, category: catName });
|
|
}}
|
|
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"
|
|
>
|
|
<option value="">Sélectionner un secteur</option>
|
|
{categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
|
<option value="Autre">Autre (proposer...)</option>
|
|
</select>
|
|
</div>
|
|
{formData.category === 'Autre' && (
|
|
<div className="sm:col-span-3 animate-in fade-in slide-in-from-top-2 duration-300">
|
|
<label className="block text-sm font-medium text-brand-600 flex items-center gap-1">
|
|
<Sparkles className="w-3 h-3" /> Nom de la nouvelle catégorie
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="suggestedCategory"
|
|
value={formData.suggestedCategory || ''}
|
|
onChange={handleInputChange}
|
|
placeholder="ex: Intelligence Artificielle"
|
|
className="mt-1 block w-full border border-brand-300 bg-brand-50 rounded-md shadow-sm py-2 px-3 focus:ring-brand-500 focus:border-brand-500 sm:text-sm"
|
|
/>
|
|
<p className="mt-1 text-[10px] text-brand-500">Elle sera vérifiée par nos administrateurs avant d'être ajoutée officiellement.</p>
|
|
</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 className="sm:col-span-6">
|
|
<TagInput
|
|
value={formData.tags || []}
|
|
onChange={(tags) => setFormData({ ...formData, tags })}
|
|
suggestions={allTags}
|
|
onlySuggestions={true}
|
|
label="Mots-clés & Tags"
|
|
placeholder="Choisir dans la liste..."
|
|
/>
|
|
<p className="mt-1 text-[10px] text-gray-500 italic">Ces tags aideront les clients à trouver votre boutique via la recherche.</p>
|
|
</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-3">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Pays</label>
|
|
<select
|
|
name="countryId"
|
|
value={formData.countryId || ''}
|
|
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"
|
|
>
|
|
<option value="">Sélectionner un pays</option>
|
|
{countries.map(c => (
|
|
<option key={c.id} value={c.id}>{c.flag} {c.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="sm:col-span-3">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Ville</label>
|
|
<input
|
|
type="text"
|
|
name="city"
|
|
value={formData.city || ''}
|
|
onChange={handleInputChange}
|
|
placeholder="Ex: Abidjan"
|
|
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-6 hidden">
|
|
{/* Keep legacy field for compatibility if needed, but hide it */}
|
|
<input type="hidden" name="location" value={`${formData.city || ''}, ${countries.find(c => c.id === formData.countryId)?.name || ''}`} />
|
|
</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>
|
|
|
|
{/* D. Zone de Danger */}
|
|
<div className="bg-red-50 border border-red-200 shadow-sm rounded-lg p-6">
|
|
<div className="flex items-center gap-2 mb-4 text-red-800">
|
|
<AlertCircle className="w-5 h-5" />
|
|
<h3 className="text-lg font-bold">Zone de Danger</h3>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div>
|
|
<p className="text-sm font-bold text-red-900">Supprimer définitivement la boutique</p>
|
|
<p className="text-xs text-red-700 mt-1">
|
|
Une fois supprimée, votre boutique ne sera plus visible dans l'annuaire.
|
|
Toutes vos données (offres, messages, avis) seront perdues.
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={handleDelete}
|
|
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 font-bold text-sm shadow-sm transition-all active:scale-95 whitespace-nowrap"
|
|
>
|
|
Supprimer ma boutique
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DashboardProfile;
|