Skip to content

Added feature to upload image by URL #3488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,8 @@ export const SET_COOKIE_CONSENT = 'SET_COOKIE_CONSENT';

export const CONSOLE_EVENT = 'CONSOLE_EVENT';
export const CLEAR_CONSOLE = 'CLEAR_CONSOLE';

export const OPEN_UPLOAD_IMAGE_BY_URL_MODAL = 'OPEN_UPLOAD_IMAGE_BY_URL_MODAL';
export const CLOSE_UPLOAD_IMAGE_BY_URL_MODAL =
'CLOSE_UPLOAD_IMAGE_BY_URL_MODAL';
export const ADD_SKETCH_FILE = 'ADD_SKETCH_FILE';
13 changes: 13 additions & 0 deletions client/modules/IDE/actions/ide.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ export function closeUploadFileModal() {
};
}

export function openUploadImageByUrlModal(parentId) {
return {
type: ActionTypes.OPEN_UPLOAD_IMAGE_BY_URL_MODAL,
parentId
};
}

export function closeUploadImageByUrlModal() {
return {
type: ActionTypes.CLOSE_UPLOAD_IMAGE_BY_URL_MODAL
};
}

export function expandSidebar() {
return {
type: ActionTypes.EXPAND_SIDEBAR
Expand Down
39 changes: 37 additions & 2 deletions client/modules/IDE/components/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import {
newFile,
newFolder,
openProjectOptions,
openUploadFileModal
openUploadFileModal,
openUploadImageByUrlModal
} from '../actions/ide';
import { selectRootFile } from '../selectors/files';
import { getAuthenticated, selectCanEditSketch } from '../selectors/users';

import ConnectedFileNode from './FileNode';
import { PlusIcon } from '../../../common/icons';
import { FileDrawer } from './Editor/MobileEditor';

import UploadMediaModal from './UploadMediaModal';
// TODO: use a generic Dropdown UI component

export default function SideBar() {
Expand All @@ -31,6 +32,9 @@ export default function SideBar() {
const isExpanded = useSelector((state) => state.ide.sidebarIsExpanded);
const canEditProject = useSelector(selectCanEditSketch);
const isAuthenticated = useSelector(getAuthenticated);
const isUploadImageByUrlModalOpen = useSelector(
(state) => state.ide.uploadImageByUrlModalVisible
);

const sidebarOptionsRef = useRef(null);

Expand Down Expand Up @@ -137,11 +141,42 @@ export default function SideBar() {
</button>
</li>
)}
{isAuthenticated && canEditProject && (
<li>
<button
aria-label={t('Sidebar.UploadImageByUrlARIA')}
onClick={() => {
dispatch(openUploadImageByUrlModal(rootFile.id));
setTimeout(() => dispatch(closeProjectOptions()), 300);
}}
>
{t('Sidebar.UploadImageByUrl')}
</button>
</li>
)}
</ul>
)}
</div>
</header>
<ConnectedFileNode id={rootFile.id} canEdit={canEditProject} />
{isUploadImageByUrlModalOpen && (
<UploadMediaModal
onUploadSuccess={(s3Url) => {
dispatch({
type: 'ADD_SKETCH_FILE',
payload: {
name: `image-${Date.now()}.jpg`,
url: s3Url,
parentId: rootFile.id
}
});
dispatch({ type: 'CLOSE_UPLOAD_IMAGE_BY_URL_MODAL' });
}}
onClose={() =>
dispatch({ type: 'CLOSE_UPLOAD_IMAGE_BY_URL_MODAL' })
}
/>
)}
</section>
</FileDrawer>
);
Expand Down
96 changes: 96 additions & 0 deletions client/modules/IDE/components/UploadMediaModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import api from '../../../utils/api';
import Button from '../../../common/Button';
import Modal from './Modal';

const UploadMediaModal = ({ onUploadSuccess, onClose }) => {
const { t } = useTranslation();
const [imageUrl, setImageUrl] = useState('');
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState(null);

const handleInputChange = (e) => {
setImageUrl(e.target.value);
if (error) setError(null);
};

const handleSubmit = async () => {
if (!imageUrl.trim()) {
setError(t('UploadMediaModal.EmptyUrlError', 'Image URL is required'));
return;
}

setIsUploading(true);
setError(null);

try {
console.log('Uploading image URL:', imageUrl);
const { s3Url } = await api.uploadImageByUrl(imageUrl);
console.log('Upload success, s3Url:', s3Url);
onUploadSuccess(s3Url);
} catch (err) {
console.error('Upload failed:', err.message, err.stack);
setError(t('UploadMediaModal.UploadError', 'Failed to upload image'));
setIsUploading(false);
}
};

return (
<Modal
title={t('UploadMediaModal.Title', 'Upload Image by URL')}
onClose={onClose}
closeAriaLabel={t(
'UploadMediaModal.CloseAriaLabel',
'Close upload media modal'
)}
contentClassName="upload-media-modal__content"
>
<div className="upload-media-modal__input-wrapper">
<label className="upload-media-modal__name-label" htmlFor="image-url">
{t('UploadMediaModal.UrlLabel', 'Image URL')}
</label>
<input
id="image-url"
type="text"
value={imageUrl}
onChange={handleInputChange}
placeholder={t('UploadMediaModal.UrlPlaceholder', 'Enter image URL')}
className="upload-media-modal__input"
disabled={isUploading}
/>
</div>

{error && <p className="upload-media-modal__error">{error}</p>}

<div className="modal__divider" />

<div className="upload-media-modal__footer">
<Button
className="upload-media-modal__button"
onClick={handleSubmit}
disabled={isUploading}
>
{isUploading
? t('UploadMediaModal.Uploading', 'Uploading...')
: t('UploadMediaModal.Upload', 'Upload')}
</Button>
<Button
className="upload-media-modal__button upload-media-modal__button--cancel"
onClick={onClose}
disabled={isUploading}
>
{t('UploadMediaModal.Cancel', 'Cancel')}
</Button>
</div>
</Modal>
);
};

UploadMediaModal.propTypes = {
onUploadSuccess: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};

export default UploadMediaModal;
32 changes: 32 additions & 0 deletions client/modules/IDE/reducers/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,38 @@ const files = (state, action) => {
return file;
});
}
case ActionTypes.ADD_SKETCH_FILE: {
const parentFile = state.find(
(file) => file.id === action.payload.parentId
);
const filePath =
parentFile.name === 'root'
? ''
: `${parentFile.filePath}/${parentFile.name}`;
const newId = objectID().toHexString();
const newState = [
...updateParent(state, {
parentId: action.payload.parentId,
id: newId
}),
{
name: action.payload.name,
id: newId,
_id: newId,
url: action.payload.url,
content: '',
children: [],
fileType: 'file',
filePath
}
];
return newState.map((file) => {
if (file.id === action.payload.parentId) {
file.children = sortedChildrenId(newState, file.children);
}
return file;
});
}
case ActionTypes.UPDATE_FILE_NAME: {
const newState = renameFile(state, action);
const updatedFile = newState.find((file) => file.id === action.id);
Expand Down
8 changes: 8 additions & 0 deletions client/modules/IDE/reducers/ide.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const initialState = {
projectOptionsVisible: false,
newFolderModalVisible: false,
uploadFileModalVisible: false,
uploadImageByUrlModalVisible: false,
shareModalVisible: false,
shareModalProjectId: 'abcd',
shareModalProjectName: 'My Cute Sketch',
Expand Down Expand Up @@ -122,6 +123,13 @@ const ide = (state = initialState, action) => {
});
case ActionTypes.CLOSE_UPLOAD_FILE_MODAL:
return Object.assign({}, state, { uploadFileModalVisible: false });
case ActionTypes.OPEN_UPLOAD_IMAGE_BY_URL_MODAL:
return Object.assign({}, state, {
uploadImageByUrlModalVisible: true,
parentId: action.parentId
});
case ActionTypes.CLOSE_UPLOAD_IMAGE_BY_URL_MODAL:
return Object.assign({}, state, { uploadImageByUrlModalVisible: false });
default:
return state;
}
Expand Down
75 changes: 65 additions & 10 deletions client/styles/components/_modal.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
@use "sass:math";

$base-font-size: 16;

.modal {
position: absolute;
top: #{math.div(60, $base-font-size)}rem;
right: 50%;
transform: translate(50%, 0);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: #{math.div(60, $base-font-size)}rem;
z-index: 100;
outline: none;
}
Expand All @@ -18,16 +26,20 @@
width: #{math.div(500, $base-font-size)}rem;
}

.modal--reduced & {
//min-height: #{150 / $base-font-size}rem;
}
// .modal--reduced & {
// //min-height: #{150 / $base-font-size}rem;
// }
}

.modal-content-folder {
@extend .modal-content;
height: #{math.div(150, $base-font-size)}rem;
}

.upload-media-modal__content {
text-align: center;
}

.modal__exit-button {
@include icon();
}
Expand All @@ -38,20 +50,63 @@
margin-bottom: #{math.div(20, $base-font-size)}rem;
}

.new-folder-form__input-wrapper, .new-file-form__input-wrapper {
.modal__title {
font-size: #{math.div(20, $base-font-size)}rem;
}

.new-folder-form__input-wrapper,
.new-file-form__input-wrapper,
.upload-media-modal__input-wrapper {
display: flex;
margin-bottom: #{math.div(20, $base-font-size)}rem;
}

.new-file-form__name-label, .new-folder-form__name-label {
.new-file-form__name-label,
.new-folder-form__name-label,
.upload-media-modal__name-label {
@extend %hidden-element;
}

.new-file-form__name-input, .new-folder-form__name-input {
.new-file-form__name-input,
.new-folder-form__name-input,
.upload-media-modal__input {
margin-right: #{math.div(10, $base-font-size)}rem;
flex: 1;
width: 100%;
padding: #{math.div(8, $base-font-size)}rem;
border: #{math.div(1, $base-font-size)}rem solid #ccc;
box-sizing: border-box;
}

.upload-media-modal__error {
margin: #{math.div(10, $base-font-size)}rem 0;
font-size: #{math.div(14, $base-font-size)}rem;
}

.modal__divider {
text-align: center;
margin: #{math.div(20, $base-font-size)}rem 0;
border-top: #{math.div(1, $base-font-size)}rem solid #ccc;
}

.upload-media-modal__footer {
display: flex;
justify-content: center;
}

.upload-media-modal__button {
padding: #{math.div(8, $base-font-size)}rem #{math.div(16, $base-font-size)}rem;
margin: #{math.div(5, $base-font-size)}rem;
border: none;
cursor: pointer;
font-size: #{math.div(14, $base-font-size)}rem;

&--cancel {
margin-left: #{math.div(10, $base-font-size)}rem;
}

&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
14 changes: 14 additions & 0 deletions client/utils/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import axios from 'axios';

const api = {
uploadImageByUrl: async (imageUrl) => {
const response = await axios.post('/api/media/upload-by-url', { imageUrl });
return response.data;
},
updateSketchFiles: async ({ sketchId, files }) => {
const response = await axios.put(`/api/sketches/${sketchId}`, { files });
return response.data;
}
};

export default api;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@
"@redux-devtools/log-monitor": "^4.0.2",
"@reduxjs/toolkit": "^1.9.3",
"async": "^3.2.3",
"axios": "^1.8.2",
"aws-sdk": "^2.1692.0",
"axios": "^1.9.0",
"babel-plugin-styled-components": "^1.13.2",
"bcryptjs": "^2.4.3",
"blob-util": "^1.2.1",
Expand Down
Loading