A comprehensive image upload component with drag-and-drop support, cropping functionality, and cloud storage integration.
src/components/ImageDropzoneWithCropper.tsx
ImageDropzoneWithCropper provides a complete image upload solution with drag-and-drop interface, image cropping capabilities, file management, and integration with cloud storage services.
interface DropzoneWithCropperProps {
prefillFile?: IServerFile | null;
disabled?: boolean;
label?: string;
Icon?: React.ReactNode;
enableCropper?: boolean;
uploadDirectory?: DIRECTORY_NAME;
uploadUniqueFileName?: boolean;
onUploadComplete?: (serverFile: IServerFile) => void;
onFileDeleteComplete?: () => void;
aspectRatio?: number;
}prefillFile: Pre-existing file to displaydisabled: Disable upload functionalitylabel: Custom dropzone label textIcon: Custom upload iconenableCropper: Enable image cropping modaluploadDirectory: Target upload directoryuploadUniqueFileName: Generate unique filenamesonUploadComplete: Callback when upload succeedsonFileDeleteComplete: Callback when file is deletedaspectRatio: Crop aspect ratio (default: 1)
import ImageDropzoneWithCropper from '@/components/ImageDropzoneWithCropper';
import { DIRECTORY_NAME } from '@/backend/models/domain-models';
function ProfilePhotoUpload() {
const handleUploadComplete = (file: IServerFile) => {
console.log('Upload completed:', file);
// Update user profile photo
};
return (
<ImageDropzoneWithCropper
uploadDirectory={DIRECTORY_NAME.PROFILE_PHOTOS}
onUploadComplete={handleUploadComplete}
aspectRatio={1}
label="Upload profile photo"
/>
);
}function ArticleCoverUpload() {
const [coverImage, setCoverImage] = useState<IServerFile | null>(null);
return (
<ImageDropzoneWithCropper
enableCropper={true}
aspectRatio={16/9}
uploadDirectory={DIRECTORY_NAME.ARTICLE_IMAGES}
onUploadComplete={setCoverImage}
prefillFile={coverImage}
onFileDeleteComplete={() => setCoverImage(null)}
label="Upload article cover"
/>
);
}function GalleryImageUpload() {
const [images, setImages] = useState<IServerFile[]>([]);
const handleUpload = (file: IServerFile) => {
setImages(prev => [...prev, file]);
};
return (
<div className="grid grid-cols-2 gap-4">
<ImageDropzoneWithCropper
uploadDirectory={DIRECTORY_NAME.GALLERY}
onUploadComplete={handleUpload}
uploadUniqueFileName={true}
aspectRatio={4/3}
/>
{images.map((image, index) => (
<ImageDropzoneWithCropper
key={index}
prefillFile={image}
onFileDeleteComplete={() => {
setImages(prev => prev.filter((_, i) => i !== index));
}}
/>
))}
</div>
);
}- Visual feedback: Highlighting on drag over
- File validation: Accepts only image files
- Error states: Visual indication of rejected files
- Accessibility: Keyboard navigation support
- Advanced cropper: Uses react-advanced-cropper library
- Aspect ratio control: Configurable aspect ratios
- Image manipulation: Flip, rotate, and crop operations
- Grid overlay: Visual crop guidelines
- Real-time preview: Live crop preview
- Upload progress: Loading states during upload
- Delete functionality: Remove uploaded files
- File preview: Display uploaded images
- Error handling: Upload failure management
// Horizontal flip
const flip = (horizontal: boolean, vertical: boolean) => {
cropperRef.current?.flipImage(horizontal, vertical);
};
// Rotation (90-degree increments)
const rotate = (angle: number) => {
cropperRef.current?.rotateImage(angle);
};- Flip horizontal: Mirror image horizontally
- Flip vertical: Mirror image vertically
- Rotate 90°: Rotate image clockwise
- Aspect ratio: Maintain consistent proportions
- Grid overlay: Visual cropping guides
- User drops/selects image
- File validation
- Direct upload to storage
- Callback with file information
- User drops/selects image
- Convert to base64 for preview
- Open cropping modal
- User adjusts crop/rotation
- Generate cropped blob
- Upload processed image
- Callback with file information
// R2 (Cloudflare)
{
provider: "r2",
key: "unique-filename.jpg"
}
// Directory-based organization
uploadDirectory: DIRECTORY_NAME.PROFILE_PHOTOS
// Results in: "profile-photos/unique-filename.jpg"uploadFile({
files: [file],
directory: DIRECTORY_NAME.ARTICLE_IMAGES,
generateUniqueFileName: true
})useServerFile: File upload/delete operationsuseToggle: Modal state management
useRef: Cropper instance referenceuseState: Base64 image state
// Dropzone with upload prompt
<div className="dropzone">
<UploadIcon />
<p>Drop file here</p>
</div>// Loading indicator during upload
{uploading && (
<div>
<Loader className="animate-spin" />
<p>Uploading...</p>
</div>
)}// Display uploaded image with delete option
<div className="relative">
<img src={getFileUrl(prefillFile)} />
<button onClick={handleDelete}>
<TrashIcon />
</button>
</div><Dialog open={modalOpen} onOpenChange={modelHandler.close}>
<DialogContent>
<Cropper
ref={cropperRef}
src={base64}
stencilProps={{ grid: true, aspectRatio }}
/>
<div className="controls">
{/* Flip and rotate buttons */}
<Button onClick={handleUpload}>Upload</Button>
</div>
</DialogContent>
</Dialog>- Get canvas from cropper
- Convert canvas to blob
- Upload blob to storage
- Return file information
- Close modal
// Dynamic classes based on state
className={clsx(
"grid w-full h-full p-4 border border-dotted rounded-md",
{
"bg-green-100": isFileDialogActive,
"bg-primary": isDragReject,
"cursor-not-allowed": disabled,
}
)}- Adaptive aspect ratios
- Mobile-friendly touch controls
- Responsive modal sizing
- Optimized for various screen sizes
.catch((err) => {
console.log(err);
alert("Error uploading file");
});- Image-only file acceptance
- File size limitations (via dropzone config)
- MIME type validation
- Error state visual feedback
- Keyboard navigation: Full keyboard support
- Screen reader support: Proper ARIA labels
- Focus management: Logical tab order
- Error announcements: Screen reader error feedback
- Client-side cropping: Reduces server load
- Blob conversion: Efficient binary handling
- Canvas optimization: Memory-efficient processing
- Unique filenames: Prevents conflicts
- Directory organization: Structured storage
- Progress indication: User feedback during upload
<ImageDropzoneWithCropper
enableCropper={true}
aspectRatio={1}
uploadDirectory={DIRECTORY_NAME.PROFILE_PHOTOS}
onUploadComplete={(file) => updateUserProfile({ photo: file })}
/><ImageDropzoneWithCropper
enableCropper={true}
aspectRatio={16/9}
uploadDirectory={DIRECTORY_NAME.ARTICLE_IMAGES}
onUploadComplete={(file) => setArticleCover(file)}
label="Upload article cover (16:9 ratio)"
/><ImageDropzoneWithCropper
enableCropper={true}
aspectRatio={4/3}
uploadDirectory={DIRECTORY_NAME.PRODUCTS}
uploadUniqueFileName={true}
onUploadComplete={addProductImage}
/>react-dropzone: Drag and drop functionalityreact-advanced-cropper: Image cropping capabilitieslucide-react: Icons for UI elements
useServerFile: Upload/delete functionalityuseToggle: Modal state managementgetFileUrl: File URL generation utility
// Always handle upload completion
onUploadComplete={(file) => {
// Update application state
setImageFile(file);
// Show success message
showSuccess("Image uploaded successfully");
}}
// Handle deletion properly
onFileDeleteComplete={() => {
// Clear application state
setImageFile(null);
// Show confirmation
showInfo("Image deleted");
}}// Provide clear labels
label="Drop your profile photo here or click to browse"
// Use appropriate aspect ratios
aspectRatio={16/9} // For covers
aspectRatio={1} // For avatars
aspectRatio={4/3} // For general images
// Enable cropping for precise control
enableCropper={true}// Validate files before upload
accept={{ "image/*": [".jpeg", ".jpg", ".png", ".webp"] }}
// Handle network errors gracefully
.catch((error) => {
showError("Upload failed. Please try again.");
console.error("Upload error:", error);
});