feat: implement business detail client view with interactive features including rating, favoriting, messaging, and analytics tracking
This commit is contained in:
@@ -7,6 +7,8 @@ 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) => {
|
||||
@@ -54,6 +56,12 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
|
||||
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 () => {
|
||||
@@ -112,6 +120,74 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
|
||||
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', {
|
||||
@@ -233,19 +309,94 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
|
||||
{/* 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="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-gray-200" src={formData.logoUrl} alt="Logo actuel" />
|
||||
<span className="text-[10px] font-bold text-gray-400 uppercase">Logo</span>
|
||||
<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 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">Modifier le logo</p>
|
||||
|
||||
<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">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Photo de Couverture (Bannière)</label>
|
||||
<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
|
||||
@@ -263,12 +414,33 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
|
||||
<span className="text-xs">Aucune bannière personnalisée</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
<button className="bg-white text-gray-900 px-4 py-1.5 rounded-full text-xs font-bold shadow-lg">Changer la photo</button>
|
||||
|
||||
{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
|
||||
@@ -307,14 +479,16 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
/>
|
||||
{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">
|
||||
@@ -389,8 +563,9 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
|
||||
value={formData.tags || []}
|
||||
onChange={(tags) => setFormData({ ...formData, tags })}
|
||||
suggestions={allTags}
|
||||
onlySuggestions={true}
|
||||
label="Mots-clés & Tags"
|
||||
placeholder="innovation, tech, artisanat..."
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user