refactor(ui): migrar componente ImageUpload a HeroUI
This commit is contained in:
1 parent
dcab297355
commit
67b96442a3
1 file changed
+141
-27
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { FiUpload, FiX } from "react-icons/fi";
|
||||
import { Button, CloseButton, Label } from "@heroui/react";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
|
||||
export interface ImageUploadProps {
|
||||
@@ -11,8 +13,15 @@ export interface ImageUploadProps {
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export function ImageUpload({ label, value, onChange, aspectRatio = "square", hint }: ImageUploadProps) {
|
||||
export function ImageUpload({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
aspectRatio = "square",
|
||||
hint,
|
||||
}: ImageUploadProps) {
|
||||
const { isDark } = useTheme();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
@@ -20,34 +29,118 @@ export function ImageUpload({ label, value, onChange, aspectRatio = "square", hi
|
||||
|
||||
const handleFile = (file: File) => {
|
||||
if (!file.type.startsWith("image/")) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => onChange(e.target?.result as string);
|
||||
|
||||
reader.onload = (event) => {
|
||||
const result = event.target?.result;
|
||||
|
||||
if (typeof result === "string") {
|
||||
onChange(result);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setDragging(false);
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) handleFile(file);
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
|
||||
if (file) {
|
||||
handleFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const border = isDark ? "border-slate-700" : "border-slate-200";
|
||||
const bg = isDark ? "bg-slate-800/50" : "bg-slate-50";
|
||||
const active = dragging ? (isDark ? "border-violet-500 bg-violet-500/10" : "border-violet-400 bg-violet-50") : "";
|
||||
const muted = isDark ? "text-slate-400" : "text-slate-500";
|
||||
const borderColor = isDark
|
||||
? "border-slate-700"
|
||||
: "border-slate-200";
|
||||
|
||||
const backgroundColor = isDark
|
||||
? "bg-slate-800/50"
|
||||
: "bg-slate-50";
|
||||
|
||||
const dragActiveStyles = dragging
|
||||
? isDark
|
||||
? "border-violet-500 bg-violet-500/10"
|
||||
: "border-violet-400 bg-violet-50"
|
||||
: "";
|
||||
|
||||
const mutedColor = isDark
|
||||
? "text-slate-400"
|
||||
: "text-slate-500";
|
||||
|
||||
const labelColor = isDark
|
||||
? "text-slate-300"
|
||||
: "text-slate-700";
|
||||
|
||||
const pickerSize = isBanner
|
||||
? "h-32 w-full"
|
||||
: "h-28 w-28";
|
||||
|
||||
const openFilePicker = () => {
|
||||
inputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleInputChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const file = event.target.files?.[0];
|
||||
|
||||
if (file) {
|
||||
handleFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
onChange(undefined);
|
||||
|
||||
// Permite volver a seleccionar la misma imagen
|
||||
// después de eliminarla.
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className={`block text-sm font-medium mb-1.5 ${isDark ? "text-slate-300" : "text-slate-700"}`}>
|
||||
<Label
|
||||
className={`mb-1.5 block text-sm font-medium ${labelColor}`}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</Label>
|
||||
|
||||
<div
|
||||
className={`relative rounded-xl border-2 border-dashed transition-all duration-200 overflow-hidden cursor-pointer ${border} ${bg} ${active} ${isBanner ? "h-32" : "h-28 w-28"}`}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
onDragOver={(e) => { e.preventDefault(); setDragging(true); }}
|
||||
onDragLeave={() => setDragging(false)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`Seleccionar imagen para ${label}`}
|
||||
className={[
|
||||
"relative cursor-pointer overflow-hidden rounded-xl",
|
||||
"border-2 border-dashed",
|
||||
"transition-all duration-200",
|
||||
"focus-visible:outline-none focus-visible:ring-2",
|
||||
"focus-visible:ring-violet-500",
|
||||
borderColor,
|
||||
backgroundColor,
|
||||
dragActiveStyles,
|
||||
pickerSize,
|
||||
].join(" ")}
|
||||
onClick={openFilePicker}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
openFilePicker();
|
||||
}
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
setDragging(true);
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setDragging(false);
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{value ? (
|
||||
@@ -55,34 +148,55 @@ export function ImageUpload({ label, value, onChange, aspectRatio = "square", hi
|
||||
<img
|
||||
src={value}
|
||||
alt={label}
|
||||
className="w-full h-full object-cover"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); onChange(undefined); }}
|
||||
className="absolute top-2 right-2 p-1 rounded-full bg-black/60 text-white hover:bg-black/80 transition"
|
||||
|
||||
<div
|
||||
className="absolute right-2 top-2"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<CloseButton
|
||||
aria-label="Eliminar imagen"
|
||||
onPress={handleRemove}
|
||||
className="rounded-full bg-black/60 text-white hover:bg-black/80"
|
||||
>
|
||||
<FiX className="text-xs" />
|
||||
</button>
|
||||
</CloseButton>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-1.5 px-4 text-center">
|
||||
<FiUpload className={`text-xl ${muted}`} />
|
||||
<p className={`text-xs ${muted}`}>
|
||||
<div className="flex h-full flex-col items-center justify-center gap-1.5 px-4 text-center">
|
||||
<FiUpload className={`text-xl ${mutedColor}`} />
|
||||
|
||||
<p className={`text-xs ${mutedColor}`}>
|
||||
{dragging ? "Suelta aquí" : "Click o arrastra"}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onPress={openFilePicker}
|
||||
className="mt-1"
|
||||
>
|
||||
Seleccionar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hint && <p className={`text-[11px] mt-1 ${muted}`}>{hint}</p>}
|
||||
{hint && (
|
||||
<p className={`mt-1 text-[11px] ${mutedColor}`}>
|
||||
{hint}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => { const f = e.target.files?.[0]; if (f) handleFile(f); }}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user