refactor(ui): migrar componente ImageUpload a HeroUI
This commit is contained in:
1 parent
dcab297355
commit
67b96442a3
1 file changed
+142
-28
@@ -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()}
|
||||||
>
|
>
|
||||||
<FiX className="text-xs" />
|
<CloseButton
|
||||||
</button>
|
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">
|
<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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user