feat: Add image cropping functionality with a new component, dialog, and canvas utilities.
All checks were successful
Deploy to Production / test (push) Successful in 40s
All checks were successful
Deploy to Production / test (push) Successful in 40s
This commit is contained in:
@@ -7,6 +7,7 @@ import { useState, useCallback, useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Upload, X, Image as ImageIcon, Loader2, AlertCircle } from "lucide-react";
|
||||
import { ImageCropper } from "@/components/image-cropper";
|
||||
|
||||
interface ImageUploaderProps {
|
||||
existingUrl?: string | null;
|
||||
@@ -28,6 +29,12 @@ export function ImageUploader({
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Cropper state
|
||||
const [isCropperOpen, setIsCropperOpen] = useState(false);
|
||||
const [tempImageSrc, setTempImageSrc] = useState<string | null>(null);
|
||||
const [tempFileName, setTempFileName] = useState<string>("image.jpg");
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const validateFile = useCallback((file: File): string | null => {
|
||||
@@ -50,11 +57,13 @@ export function ImageUploader({
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setTempFileName(file.name);
|
||||
|
||||
// Create preview URL
|
||||
// Read file for cropper
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setPreviewUrl(reader.result as string);
|
||||
setTempImageSrc(reader.result as string);
|
||||
setIsCropperOpen(true);
|
||||
setIsLoading(false);
|
||||
};
|
||||
reader.onerror = () => {
|
||||
@@ -62,9 +71,21 @@ export function ImageUploader({
|
||||
setIsLoading(false);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}, [validateFile]);
|
||||
|
||||
const handleCropComplete = useCallback((croppedBlob: Blob) => {
|
||||
// Convert blob back to file
|
||||
const file = new File([croppedBlob], tempFileName, { type: "image/jpeg" });
|
||||
|
||||
// Create preview
|
||||
const url = URL.createObjectURL(croppedBlob);
|
||||
setPreviewUrl(url);
|
||||
|
||||
// Pass to parent
|
||||
onFileSelect(file);
|
||||
}, [onFileSelect, validateFile]);
|
||||
setIsCropperOpen(false);
|
||||
setTempImageSrc(null);
|
||||
}, [onFileSelect, tempFileName]);
|
||||
|
||||
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -217,12 +238,20 @@ export function ImageUploader({
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-destructive flex items-center gap-1">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<ImageCropper
|
||||
isOpen={isCropperOpen}
|
||||
onClose={() => setIsCropperOpen(false)}
|
||||
onCropComplete={handleCropComplete}
|
||||
imageSrc={tempImageSrc}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user