Skip to content

Instantly share code, notes, and snippets.

@sturmenta
Last active January 27, 2025 23:55
Show Gist options
  • Save sturmenta/62ef210672e3a5b17eb37d1e83f33b37 to your computer and use it in GitHub Desktop.
Save sturmenta/62ef210672e3a5b17eb37d1e83f33b37 to your computer and use it in GitHub Desktop.
simple react image picker
import { ArrowUpFromLine, Pencil, Trash2 } from "lucide-react";
import { ChangeEvent, useEffect, useRef, useState } from "react";
import { useClickOutside } from "@/hooks/use-click-outside";
import { Touchable } from "./touchable";
export const ImagePicker = ({
id = 1, // 2, 3, ...
image,
setImage,
disabled
}: {
id?: number; // use when many image pickers
image: File | null;
setImage: (image: File | null) => void;
disabled: Boolean;
}) => {
const inputId = "input-file-" + id;
const imagePreviewOfPicker_id = "image-picker-preview-" + id;
const ref_clickOutside = useRef<HTMLDivElement>(null);
const ref_input = useRef<HTMLInputElement>(null);
useClickOutside(ref_clickOutside, () => setShowButtons(false));
const [showButtons, setShowButtons] = useState(false);
useEffect(() => {
if (image) {
const imageElement = document.getElementById(
imagePreviewOfPicker_id
) as HTMLImageElement;
imageElement.src = URL.createObjectURL(image);
}
}, [image]);
const onChangeImg = (event: ChangeEvent<HTMLInputElement>) => {
setShowButtons(false);
setImage(event.target?.files?.[0] || null);
event.target.value = '' // hack: https://github.com/ngokevin/react-file-reader-input/issues/11#issuecomment-363484861
};
const onRemoveImgIcon = () => {
setShowButtons(false);
setImage(null);
};
const onEditImgIcon = () => ref_input.current?.click();
return (
<div
ref={ref_clickOutside}
className={`relative flex h-36 w-36 items-center justify-center rounded-sm border ${disabled ? "pointer-events-none cursor-not-allowed opacity-50" : ""}`}>
<input
ref={ref_input}
id={inputId}
accept="image/jpeg, image/png"
type="file"
className={`absolute bottom-0 left-0 right-0 top-0 opacity-0 ${image ? "-z-10" : ""}`}
onChange={onChangeImg}
/>
<ArrowUpFromLine
color="gray"
className={`h-10 w-10 ${image ? "-z-10 opacity-0" : ""}`}
/>
{image && (
<img
id={imagePreviewOfPicker_id}
src={undefined}
alt="placeholder"
className="h-full w-full rounded-sm object-cover"
onClick={() => setShowButtons(true)}
/>
)}
{showButtons ? (
<div className="absolute bottom-0 left-0 right-0 top-0 z-10 flex flex-1 items-center justify-center bg-black bg-opacity-50">
<Touchable
noFlex1
className="rounded-full bg-red-600 p-2"
onClick={onEditImgIcon}>
<Pencil color="white" className="h-5 w-5" />
</Touchable>
<div className="w-5" />
<Touchable
noFlex1
className="rounded-full bg-red-600 p-2"
onClick={onRemoveImgIcon}>
<Trash2 color="white" className="h-5 w-5" />
</Touchable>
</div>
) : null}
</div>
);
};
// Usage like this:
// const [image1, setImage1] = useState<File | null>(null);
//
// <ImagePicker
// id="1"
// image={image1}
// setImage={setImage1}
// disabled={loading}
// />
export const Spinner = () => (
<svg
className="h-5 w-5 animate-spin text-red-900"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
></path>
</svg>
);
import { useState, type HTMLAttributes } from "react";
import { Spinner } from "./spinner";
type DivProps = HTMLAttributes<HTMLDivElement>;
export const Touchable = ({
children,
onClick,
className,
noFlex1,
loading: loadingFromProps,
...props
}: {
children?: React.ReactNode;
onClick?: DivProps["onClick"];
className?: DivProps["className"];
noFlex1?: boolean;
loading?: boolean;
} & DivProps) => {
const [fade, setFade] = useState(false);
const [_loading, _setLoading] = useState(false);
const loading = loadingFromProps || _loading;
return (
<div
className={`flex ${
noFlex1 ? "" : "flex-1"
} select-none items-center justify-center transition-all duration-100 ${
!fade ? "opacity-100" : "opacity-20"
} ${className} ${loading ? "cursor-wait" : "cursor-pointer"}`}
onClick={async (e) => {
if (loading) return;
_setLoading(true);
onClick && (await onClick(e));
_setLoading(false);
}}
onMouseDown={() => !loading && setFade(true)}
onMouseUp={() => !loading && setTimeout(() => setFade(false), 100)}
{...props}
>
<div className={`flex flex-1 ${loading ? "opacity-10" : ""}`}>
{children}
</div>
<div
className={`absolute self-center ${
loading ? "opacity-100" : "opacity-0"
}`}
>
<Spinner />
</div>
</div>
);
};
import { RefObject, useEffect } from "react";
export const useClickOutside = (
ref: RefObject<HTMLElement | undefined>,
callback: () => void,
addEventListener = true
) => {
const handleClick = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as HTMLElement)) {
callback();
}
};
useEffect(() => {
if (addEventListener) {
document.addEventListener("click", handleClick);
}
return () => {
document.removeEventListener("click", handleClick);
};
});
};
@sturmenta
Copy link
Author

Screen.Recording.2025-01-27.at.8.53.54.PM.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment