Some checks failed
Build and Push App / build (push) Failing after 11m28s
maj url photo + page 404 + gestion programmation d'article
175 lines
6.6 KiB
TypeScript
175 lines
6.6 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useRef } from 'react';
|
|
import { Upload, Link as LinkIcon, X, Image as ImageIcon, Loader2 } from 'lucide-react';
|
|
import { uploadImage } from '@/app/actions/upload';
|
|
import { toast } from 'react-hot-toast';
|
|
|
|
interface Props {
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
label?: string;
|
|
placeholder?: string;
|
|
name?: string;
|
|
}
|
|
|
|
export default function ImageUploader({ value, onChange, label, placeholder, name }: Props) {
|
|
const [mode, setMode] = useState<'url' | 'upload'>(value?.startsWith('/uploads/') ? 'upload' : 'url');
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
// Basic validation
|
|
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;
|
|
}
|
|
|
|
setIsUploading(true);
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
try {
|
|
const result = await uploadImage(formData);
|
|
if (result.success && result.url) {
|
|
onChange(result.url);
|
|
toast.success("Image uploadée avec succès");
|
|
} else {
|
|
toast.error(result.error || "Erreur lors de l'upload");
|
|
}
|
|
} catch (error) {
|
|
toast.error("Erreur de connexion lors de l'upload");
|
|
} finally {
|
|
setIsUploading(false);
|
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
}
|
|
};
|
|
|
|
const clearImage = () => {
|
|
onChange('');
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium text-slate-400">{label || "Image"}</label>
|
|
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-800">
|
|
<button
|
|
type="button"
|
|
onClick={() => setMode('url')}
|
|
className={`flex items-center gap-1.5 px-3 py-1 rounded-md text-xs font-medium transition-colors ${
|
|
mode === 'url' ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:text-white'
|
|
}`}
|
|
>
|
|
<LinkIcon className="w-3.5 h-3.5" />
|
|
Lien URL
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setMode('upload')}
|
|
className={`flex items-center gap-1.5 px-3 py-1 rounded-md text-xs font-medium transition-colors ${
|
|
mode === 'upload' ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:text-white'
|
|
}`}
|
|
>
|
|
<Upload className="w-3.5 h-3.5" />
|
|
Upload
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative group">
|
|
{mode === 'url' ? (
|
|
<div className="relative">
|
|
<LinkIcon className="absolute left-3 top-3.5 w-5 h-5 text-slate-500" />
|
|
<input
|
|
type="text"
|
|
name={name}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
placeholder={placeholder || "https://images.unsplash.com/..."}
|
|
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 pl-10 text-white focus:outline-none focus:border-indigo-500 transition-colors"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{!value ? (
|
|
<div
|
|
onClick={() => fileInputRef.current?.click()}
|
|
className="cursor-pointer border-2 border-dashed border-slate-700 hover:border-indigo-500 rounded-xl p-8 flex flex-col items-center justify-center gap-3 bg-slate-900/50 hover:bg-slate-900 transition-all group"
|
|
>
|
|
{isUploading ? (
|
|
<Loader2 className="w-10 h-10 text-indigo-500 animate-spin" />
|
|
) : (
|
|
<div className="w-12 h-12 rounded-full bg-slate-800 flex items-center justify-center group-hover:bg-indigo-500/20 group-hover:text-indigo-400 transition-colors">
|
|
<Upload className="w-6 h-6 text-slate-400 group-hover:text-indigo-400" />
|
|
</div>
|
|
)}
|
|
<div className="text-center">
|
|
<p className="text-sm font-medium text-white">
|
|
{isUploading ? "Upload en cours..." : "Cliquez pour uploader"}
|
|
</p>
|
|
<p className="text-xs text-slate-500 mt-1">PNG, JPG ou WEBP jusqu'à 5 Mo</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="relative rounded-xl overflow-hidden aspect-video bg-slate-900 border border-slate-700">
|
|
<img
|
|
src={value}
|
|
alt="Prévisualisation"
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => fileInputRef.current?.click()}
|
|
className="p-2 bg-white/10 backdrop-blur-md rounded-full text-white hover:bg-white/20 transition-colors"
|
|
>
|
|
<Upload className="w-5 h-5" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={clearImage}
|
|
className="p-2 bg-red-500/80 backdrop-blur-md rounded-full text-white hover:bg-red-500 transition-colors"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
{/* Hidden input for value to be sent via form if needed */}
|
|
<input type="hidden" name={name} value={value} />
|
|
</div>
|
|
)}
|
|
<input
|
|
type="file"
|
|
ref={fileInputRef}
|
|
onChange={handleFileChange}
|
|
accept="image/*"
|
|
className="hidden"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{value && mode === 'url' && (
|
|
<div className="mt-3 relative rounded-lg overflow-hidden aspect-video border border-slate-800">
|
|
<img src={value} alt="Preview" className="w-full h-full object-cover" />
|
|
<button
|
|
type="button"
|
|
onClick={clearImage}
|
|
className="absolute top-2 right-2 p-1.5 bg-black/60 backdrop-blur-md rounded-full text-white hover:bg-black transition-colors"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|