Phase 5: Photo Pre-Editor (Filerobot)
- Added react-filerobot-image-editor dependency - PhotoPreEditor component with full editing capabilities - Crop, filters, adjustments, annotations, watermark tabs - Opens after image upload, before adding to canvas - Exports edited image as PNG for canvas use Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,8 @@
|
||||
"react-konva": "^18.2.10",
|
||||
"konva": "^9.3.18",
|
||||
"use-image": "^1.1.1",
|
||||
"@xenova/transformers": "^2.17.2"
|
||||
"@xenova/transformers": "^2.17.2",
|
||||
"react-filerobot-image-editor": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
|
||||
58
client/src/components/editor/PhotoPreEditor.jsx
Normal file
58
client/src/components/editor/PhotoPreEditor.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useState } from 'react';
|
||||
import FilerobotImageEditor from 'react-filerobot-image-editor';
|
||||
|
||||
export function PhotoPreEditor({ imageSrc, onComplete, onClose }) {
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const handleComplete = (editedImageObject, designState) => {
|
||||
setSaving(true);
|
||||
|
||||
// Export the edited image
|
||||
editedImageObject.exportAsync({
|
||||
quality: 1,
|
||||
mimeType: 'image/png',
|
||||
}).then((blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
setSaving(false);
|
||||
onComplete(url);
|
||||
}).catch((error) => {
|
||||
console.error('Export failed:', error);
|
||||
setSaving(false);
|
||||
onClose();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="filerobot-overlay">
|
||||
<div className="filerobot-container">
|
||||
<FilerobotImageEditor
|
||||
source={imageSrc}
|
||||
onSave={handleComplete}
|
||||
onClose={onClose}
|
||||
annotationsCommon={{
|
||||
fill: '#ff0000',
|
||||
stroke: '#000000',
|
||||
strokeWidth: 0,
|
||||
}}
|
||||
annotations={[
|
||||
'Text',
|
||||
'Rectangle',
|
||||
'Ellipse',
|
||||
'Line',
|
||||
'Pen',
|
||||
'Eraser',
|
||||
]}
|
||||
tabs={['adjust', 'filters', 'finetune', 'annotate', 'watermark']}
|
||||
defaultTabId="adjust"
|
||||
theme={{
|
||||
accentColor: '#38bdf8',
|
||||
palettePrimary: '#38bdf8',
|
||||
}}
|
||||
saveButtonProps={{
|
||||
label: saving ? 'Exporting...' : 'Use Edited Image',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
client/src/components/editor/index.js
Normal file
1
client/src/components/editor/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { PhotoPreEditor } from './PhotoPreEditor';
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { PhotoPreEditor } from '../editor/PhotoPreEditor';
|
||||
|
||||
export function UploadTab({ onUpload }) {
|
||||
const fileInputRef = useRef(null);
|
||||
const fileInputRef = useState(null)[0];
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [editingImage, setEditingImage] = useState(null);
|
||||
|
||||
const handleFile = async (file) => {
|
||||
if (!file || !file.type.startsWith('image/')) return;
|
||||
@@ -20,7 +22,8 @@ export function UploadTab({ onUpload }) {
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
onUpload(data);
|
||||
// Open photo editor with uploaded image
|
||||
setEditingImage(data.original.url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
@@ -45,6 +48,25 @@ export function UploadTab({ onUpload }) {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleEditorComplete = (editedImageUrl) => {
|
||||
onUpload({ preview: { url: editedImageUrl } });
|
||||
setEditingImage(null);
|
||||
};
|
||||
|
||||
const handleEditorClose = () => {
|
||||
setEditingImage(null);
|
||||
};
|
||||
|
||||
if (editingImage) {
|
||||
return (
|
||||
<PhotoPreEditor
|
||||
imageSrc={editingImage}
|
||||
onComplete={handleEditorComplete}
|
||||
onClose={handleEditorClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="upload-tab">
|
||||
<h3>Upload Image</h3>
|
||||
@@ -53,7 +75,7 @@ export function UploadTab({ onUpload }) {
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
onClick={() => fileInputRef?.click()}
|
||||
>
|
||||
{uploading ? (
|
||||
<div className="uploading-state">
|
||||
@@ -65,6 +87,9 @@ export function UploadTab({ onUpload }) {
|
||||
<div className="upload-icon">📁</div>
|
||||
<p>Drop image here or click to upload</p>
|
||||
<p className="upload-hint">PNG, JPG, WEBP up to 20MB</p>
|
||||
<p className="upload-hint" style={{ marginTop: '0.5rem' }}>
|
||||
Edit with crop, filters, and effects before adding to design
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<input
|
||||
|
||||
@@ -538,6 +538,28 @@ input, textarea, select {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Filerobot Editor Overlay */
|
||||
.filerobot-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.filerobot-container {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
background: #1e1e1e;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 900px) {
|
||||
.app-layout {
|
||||
|
||||
Reference in New Issue
Block a user