Last active
January 27, 2025 23:55
-
-
Save sturmenta/62ef210672e3a5b17eb37d1e83f33b37 to your computer and use it in GitHub Desktop.
simple react image picker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | |
// /> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}; | |
}); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Screen.Recording.2025-01-27.at.8.53.54.PM.mov