No image uploaded
Installation
Follow these simple steps to add the File Trigger component to your project:
Install Dependencies
pnpm add react-dropzone
Create a new file components/ui/file-trigger.tsx
and copy the code below:
"use client";
import { useImageUpload } from "@/hooks/use-image-upload";
import { Button } from "@/components/ui/button";
import { CircleUserRoundIcon } from "lucide-react";
import Image from "next/image";
export default function Component() {
const {
previewUrl,
fileInputRef,
handleThumbnailClick: handleButtonClick,
handleFileChange,
handleRemove,
fileName,
} = useImageUpload();
return (
<div>
<div className="inline-flex items-center gap-2 align-top">
<div
className="relative flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-md border border-input"
aria-label={
previewUrl
? "Preview of uploaded image"
: "Default user avatar"
}
>
{previewUrl ? (
<Image
className="h-full w-full object-cover"
src={previewUrl}
alt="Preview of uploaded image"
width={32}
height={32}
/>
) : (
<div aria-hidden="true">
<CircleUserRoundIcon
className="opacity-60"
size={16}
/>
</div>
)}
</div>
<div className="relative inline-block">
<Button onClick={handleButtonClick} aria-haspopup="dialog">
{fileName ? "Change image" : "Upload image"}
</Button>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
accept="image/*"
aria-label="Upload image file"
/>
</div>
</div>
{fileName && (
<div className="mt-2">
<div className="inline-flex gap-2 text-xs">
<p
className="truncate text-muted-foreground"
aria-live="polite"
>
{fileName}
</p>{" "}
<button
onClick={handleRemove}
className="font-medium text-red-500 hover:underline"
aria-label={`Remove ${fileName}`}
>
Remove
</button>
</div>
</div>
)}
<div className="sr-only" aria-live="polite" role="status">
{previewUrl
? "Image uploaded and preview available"
: "No image uploaded"}
</div>
</div>
);
}
Create a new file hooks/use-file-trigger.ts
and copy the code below:
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
interface UseImageUploadProps {
onUpload?: (url: string) => void;
}
export function useImageUpload({ onUpload }: UseImageUploadProps = {}) {
const previewRef = useRef<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [fileName, setFileName] = useState<string | null>(null);
const handleThumbnailClick = useCallback(() => {
fileInputRef.current?.click();
}, []);
const handleFileChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
setFileName(file.name);
const url = URL.createObjectURL(file);
setPreviewUrl(url);
previewRef.current = url;
onUpload?.(url);
}
},
[onUpload],
);
const handleRemove = useCallback(() => {
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
setPreviewUrl(null);
setFileName(null);
previewRef.current = null;
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}, [previewUrl]);
useEffect(() => {
return () => {
if (previewRef.current) {
URL.revokeObjectURL(previewRef.current);
}
};
}, []);
return {
previewUrl,
fileName,
fileInputRef,
handleThumbnailClick,
handleFileChange,
handleRemove,
};
}
Adjust the import paths in both files according to your project's structure.