Refactor component imports and structure: Update import paths for various components to improve organization, moving them into appropriate subdirectories. Remove unused components related to user and event management, enhancing code clarity and maintainability across the application.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m36s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m36s
This commit is contained in:
225
components/layout/ImageSelector.tsx
Normal file
225
components/layout/ImageSelector.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef, type ChangeEvent } from "react";
|
||||
import { Input, Button, Card } from "@/components/ui";
|
||||
|
||||
interface ImageSelectorProps {
|
||||
value: string;
|
||||
onChange: (url: string) => void;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export default function ImageSelector({
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
}: ImageSelectorProps) {
|
||||
const [availableImages, setAvailableImages] = useState<string[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [urlInput, setUrlInput] = useState("");
|
||||
const [showGallery, setShowGallery] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAvailableImages();
|
||||
}, []);
|
||||
|
||||
const fetchAvailableImages = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/admin/images/list");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAvailableImages(data.images || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching images:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setUploading(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const response = await fetch("/api/admin/images/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
onChange(data.url);
|
||||
await fetchAvailableImages(); // Rafraîchir la liste
|
||||
} else {
|
||||
alert("Erreur lors de l'upload de l'image");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error uploading image:", error);
|
||||
alert("Erreur lors de l'upload de l'image");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleUrlSubmit = () => {
|
||||
if (urlInput.trim()) {
|
||||
onChange(urlInput.trim());
|
||||
setUrlInput("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1 break-words">
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
|
||||
{/* Colonne gauche - Image */}
|
||||
<div className="flex-shrink-0 flex justify-center sm:justify-start">
|
||||
{value ? (
|
||||
<div className="relative w-full sm:w-48 h-40 sm:h-32 border border-pixel-gold/30 rounded overflow-hidden bg-black/60">
|
||||
<img
|
||||
src={value}
|
||||
alt="Preview"
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget;
|
||||
// Ne pas boucler si l'image de fallback échoue aussi
|
||||
const currentSrc = target.src;
|
||||
const fallbackSrc = "/got-2.jpg";
|
||||
if (!currentSrc.includes(fallbackSrc)) {
|
||||
target.src = fallbackSrc;
|
||||
} else {
|
||||
target.style.display = "none";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => onChange("")}
|
||||
className="absolute top-2 right-2 px-2 py-1 bg-red-900/80 text-red-200 text-xs rounded hover:bg-red-900 transition"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full sm:w-48 h-40 sm:h-32 border border-pixel-gold/30 rounded bg-black/60 flex items-center justify-center">
|
||||
<span className="text-xs text-gray-500">Aucune</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Colonne droite - Contrôles */}
|
||||
<div className="flex-1 space-y-3 min-w-0">
|
||||
{/* Input URL */}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={urlInput}
|
||||
onChange={(e) => setUrlInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === "Enter" && handleUrlSubmit()}
|
||||
placeholder="https://example.com/image.jpg ou /image.jpg"
|
||||
className="flex-1 text-xs sm:text-sm px-3 py-2 min-w-0"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleUrlSubmit}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
URL
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Upload depuis le disque */}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileUpload}
|
||||
className="hidden"
|
||||
id={`file-${label}`}
|
||||
/>
|
||||
<label htmlFor={`file-${label}`}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
as="span"
|
||||
disabled={uploading}
|
||||
className="flex-1 text-center cursor-pointer"
|
||||
>
|
||||
{uploading ? "Upload..." : "Upload depuis le disque"}
|
||||
</Button>
|
||||
</label>
|
||||
<Button
|
||||
onClick={() => setShowGallery(!showGallery)}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{showGallery ? "Masquer" : "Galerie"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Chemin de l'image */}
|
||||
{value && (
|
||||
<p className="text-xs text-gray-400 truncate break-all">{value}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Galerie d'images */}
|
||||
{showGallery && (
|
||||
<Card variant="dark" className="mt-4 p-3 sm:p-4">
|
||||
<h4 className="text-xs sm:text-sm text-gray-300 mb-3">
|
||||
Images disponibles
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2 sm:gap-3 max-h-64 overflow-y-auto">
|
||||
{availableImages.length === 0 ? (
|
||||
<div className="col-span-full text-center text-gray-400 text-sm py-4">
|
||||
Aucune image disponible
|
||||
</div>
|
||||
) : (
|
||||
availableImages.map((imageUrl) => (
|
||||
<button
|
||||
key={imageUrl}
|
||||
onClick={() => {
|
||||
onChange(imageUrl);
|
||||
setShowGallery(false);
|
||||
}}
|
||||
className={`relative aspect-video rounded overflow-hidden border-2 transition ${
|
||||
value === imageUrl
|
||||
? "border-pixel-gold"
|
||||
: "border-pixel-gold/30 hover:border-pixel-gold/50"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={imageUrl}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
{value === imageUrl && (
|
||||
<div className="absolute inset-0 bg-pixel-gold/20 flex items-center justify-center">
|
||||
<span className="text-pixel-gold text-xs">✓</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user