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",
|
"react-konva": "^18.2.10",
|
||||||
"konva": "^9.3.18",
|
"konva": "^9.3.18",
|
||||||
"use-image": "^1.1.1",
|
"use-image": "^1.1.1",
|
||||||
"@xenova/transformers": "^2.17.2"
|
"@xenova/transformers": "^2.17.2",
|
||||||
|
"react-filerobot-image-editor": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@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 }) {
|
export function UploadTab({ onUpload }) {
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useState(null)[0];
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [editingImage, setEditingImage] = useState(null);
|
||||||
|
|
||||||
const handleFile = async (file) => {
|
const handleFile = async (file) => {
|
||||||
if (!file || !file.type.startsWith('image/')) return;
|
if (!file || !file.type.startsWith('image/')) return;
|
||||||
@@ -20,7 +22,8 @@ export function UploadTab({ onUpload }) {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
onUpload(data);
|
// Open photo editor with uploaded image
|
||||||
|
setEditingImage(data.original.url);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload failed:', error);
|
console.error('Upload failed:', error);
|
||||||
@@ -45,6 +48,25 @@ export function UploadTab({ onUpload }) {
|
|||||||
setIsDragging(false);
|
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 (
|
return (
|
||||||
<div className="upload-tab">
|
<div className="upload-tab">
|
||||||
<h3>Upload Image</h3>
|
<h3>Upload Image</h3>
|
||||||
@@ -53,7 +75,7 @@ export function UploadTab({ onUpload }) {
|
|||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef?.click()}
|
||||||
>
|
>
|
||||||
{uploading ? (
|
{uploading ? (
|
||||||
<div className="uploading-state">
|
<div className="uploading-state">
|
||||||
@@ -65,6 +87,9 @@ export function UploadTab({ onUpload }) {
|
|||||||
<div className="upload-icon">📁</div>
|
<div className="upload-icon">📁</div>
|
||||||
<p>Drop image here or click to upload</p>
|
<p>Drop image here or click to upload</p>
|
||||||
<p className="upload-hint">PNG, JPG, WEBP up to 20MB</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
|
<input
|
||||||
|
|||||||
@@ -538,6 +538,28 @@ input, textarea, select {
|
|||||||
animation: spin 1s linear infinite;
|
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 */
|
/* Responsive */
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.app-layout {
|
.app-layout {
|
||||||
|
|||||||
Reference in New Issue
Block a user