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
+142 -28
+142 -28
View File
@@ -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()}
>
<FiX className="text-xs" />
</button>
<CloseButton
aria-label="Eliminar imagen"
onPress={handleRemove}
className="rounded-full bg-black/60 text-white hover:bg-black/80"
>
<FiX className="text-xs" />
</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>
);