refactor(ui): migrar componente ImageUpload a HeroUI

This commit is contained in:
Miguel Ortega committed 2026-09-14 00:05:36 -05:00
1 parent dcab297355
commit 67b96442a3
1 file changed
+141 -27
+141 -27
View File
@@ -1,6 +1,8 @@
"use client"; "use client";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { FiUpload, FiX } from "react-icons/fi"; import { FiUpload, FiX } from "react-icons/fi";
import { Button, CloseButton, Label } from "@heroui/react";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
export interface ImageUploadProps { export interface ImageUploadProps {
@@ -11,8 +13,15 @@ export interface ImageUploadProps {
hint?: string; 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 { isDark } = useTheme();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
@@ -20,34 +29,118 @@ export function ImageUpload({ label, value, onChange, aspectRatio = "square", hi
const handleFile = (file: File) => { const handleFile = (file: File) => {
if (!file.type.startsWith("image/")) return; if (!file.type.startsWith("image/")) return;
const reader = new FileReader(); 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); reader.readAsDataURL(file);
}; };
const handleDrop = (e: React.DragEvent) => { const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
e.preventDefault(); event.preventDefault();
setDragging(false); 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 borderColor = isDark
const bg = isDark ? "bg-slate-800/50" : "bg-slate-50"; ? "border-slate-700"
const active = dragging ? (isDark ? "border-violet-500 bg-violet-500/10" : "border-violet-400 bg-violet-50") : ""; : "border-slate-200";
const muted = isDark ? "text-slate-400" : "text-slate-500";
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 ( return (
<div> <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> </Label>
<div <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"}`} role="button"
onClick={() => inputRef.current?.click()} tabIndex={0}
onDragOver={(e) => { e.preventDefault(); setDragging(true); }} aria-label={`Seleccionar imagen para ${label}`}
onDragLeave={() => setDragging(false)} 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} onDrop={handleDrop}
> >
{value ? ( {value ? (
@@ -55,34 +148,55 @@ export function ImageUpload({ label, value, onChange, aspectRatio = "square", hi
<img <img
src={value} src={value}
alt={label} alt={label}
className="w-full h-full object-cover" className="h-full w-full object-cover"
/> />
<button
type="button" <div
onClick={(e) => { e.stopPropagation(); onChange(undefined); }} className="absolute right-2 top-2"
className="absolute top-2 right-2 p-1 rounded-full bg-black/60 text-white hover:bg-black/80 transition" 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" /> <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"> <div className="flex h-full flex-col items-center justify-center gap-1.5 px-4 text-center">
<FiUpload className={`text-xl ${muted}`} /> <FiUpload className={`text-xl ${mutedColor}`} />
<p className={`text-xs ${muted}`}>
<p className={`text-xs ${mutedColor}`}>
{dragging ? "Suelta aquí" : "Click o arrastra"} {dragging ? "Suelta aquí" : "Click o arrastra"}
</p> </p>
<Button
type="button"
variant="ghost"
size="sm"
onPress={openFilePicker}
className="mt-1"
>
Seleccionar
</Button>
</div> </div>
)} )}
</div> </div>
{hint && <p className={`text-[11px] mt-1 ${muted}`}>{hint}</p>} {hint && (
<p className={`mt-1 text-[11px] ${mutedColor}`}>
{hint}
</p>
)}
<input <input
ref={inputRef} ref={inputRef}
type="file" type="file"
accept="image/*" accept="image/*"
className="hidden" className="hidden"
onChange={(e) => { const f = e.target.files?.[0]; if (f) handleFile(f); }} onChange={handleInputChange}
/> />
</div> </div>
); );