feat(29-03): integrate crop editor into ImageUpload
Show ImageCropEditor after successful upload when onCropChange callback is provided. Editor replaces image preview temporarily. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { apiUpload } from "../lib/api";
|
import { apiUpload } from "../lib/api";
|
||||||
import { GearImage, imageContainerBg } from "./GearImage";
|
import { GearImage, imageContainerBg } from "./GearImage";
|
||||||
|
import { ImageCropEditor } from "./ImageCropEditor";
|
||||||
|
|
||||||
interface ImageUploadProps {
|
interface ImageUploadProps {
|
||||||
value: string | null;
|
value: string | null;
|
||||||
imageUrl?: string | null;
|
imageUrl?: string | null;
|
||||||
dominantColor?: string | null;
|
dominantColor?: string | null;
|
||||||
onChange: (filename: string | null) => void;
|
onChange: (filename: string | null) => void;
|
||||||
|
onCropChange?: (crop: { zoom: number; x: number; y: number }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_SIZE_BYTES = 5 * 1024 * 1024; // 5MB
|
const MAX_SIZE_BYTES = 5 * 1024 * 1024; // 5MB
|
||||||
@@ -17,10 +19,12 @@ export function ImageUpload({
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
dominantColor,
|
dominantColor,
|
||||||
onChange,
|
onChange,
|
||||||
|
onCropChange,
|
||||||
}: ImageUploadProps) {
|
}: ImageUploadProps) {
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [localPreview, setLocalPreview] = useState<string | null>(null);
|
const [localPreview, setLocalPreview] = useState<string | null>(null);
|
||||||
|
const [showCropEditor, setShowCropEditor] = useState(false);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
@@ -47,6 +51,9 @@ export function ImageUpload({
|
|||||||
try {
|
try {
|
||||||
const result = await apiUpload<{ filename: string }>("/api/images", file);
|
const result = await apiUpload<{ filename: string }>("/api/images", file);
|
||||||
onChange(result.filename);
|
onChange(result.filename);
|
||||||
|
if (onCropChange) {
|
||||||
|
setShowCropEditor(true);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setError("Upload failed. Please try again.");
|
setError("Upload failed. Please try again.");
|
||||||
setLocalPreview(null);
|
setLocalPreview(null);
|
||||||
@@ -69,7 +76,23 @@ export function ImageUpload({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{/* Crop editor overlay */}
|
||||||
|
{showCropEditor && displayUrl && onCropChange && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<ImageCropEditor
|
||||||
|
imageUrl={displayUrl}
|
||||||
|
dominantColor={dominantColor}
|
||||||
|
onSave={(result) => {
|
||||||
|
onCropChange(result);
|
||||||
|
setShowCropEditor(false);
|
||||||
|
}}
|
||||||
|
onCancel={() => setShowCropEditor(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Hero image area */}
|
{/* Hero image area */}
|
||||||
|
{!showCropEditor && (
|
||||||
<div
|
<div
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
className="relative w-full aspect-[4/3] rounded-xl overflow-hidden cursor-pointer group"
|
className="relative w-full aspect-[4/3] rounded-xl overflow-hidden cursor-pointer group"
|
||||||
@@ -154,6 +177,7 @@ export function ImageUpload({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|||||||
Reference in New Issue
Block a user