diff --git a/src/components/RichTextEditor/Image/ImageModal.jsx b/src/components/RichTextEditor/Image/ImageModal.jsx
index 4fdb91386..e728967fa 100644
--- a/src/components/RichTextEditor/Image/ImageModal.jsx
+++ b/src/components/RichTextEditor/Image/ImageModal.jsx
@@ -1,27 +1,26 @@
/* eslint-disable react/forbid-prop-types */
import React, { useState } from 'react';
-import { FormattedMessage, injectIntl } from 'react-intl';
+import { FormattedMessage, useIntl } from 'react-intl';
import PropTypes from 'prop-types';
-import { ControlLabel, HelpBlock, Image } from 'react-bootstrap';
-import { Button, Dialog } from 'hds-react';
-import Dropzone from 'react-dropzone';
-import FormControl from 'react-bootstrap/lib/FormControl';
+import { ControlLabel } from 'react-bootstrap';
+import { Button, Card, Dialog, FileInput, TextInput } from 'hds-react';
import { useDispatch } from 'react-redux';
import getMessage from '../../../utils/getMessage';
import { isFormValid } from '../../../utils/iframeUtils';
-import Icon from '../../../utils/Icon';
-import { createLocalizedNotificationPayload, NOTIFICATION_TYPES } from '../../../utils/notify';
+import compressFile from '../../../utils/images/compressFile';
+import fileToDataUri from '../../../utils/images/fileToDataUri';
import { addToast } from '../../../actions/toast';
+import { createLocalizedNotificationPayload, NOTIFICATION_TYPES } from '../../../utils/notify';
/**
* MAX_IMAGE_SIZE given in bytes
*/
-const MAX_IMAGE_SIZE = 999999;
-
-const ImageModal = (props) => {
+const MAX_IMAGE_SIZE = 1;
+const ImageModal = ({ isOpen, onClose, onSubmit }) => {
const dispatch = useDispatch();
+ const intl = useIntl();
const [showFormErrorMsg, setShowFormErrorMsg] = useState(false);
const [fileReaderResult, setFileReaderResult] = useState(false);
@@ -31,35 +30,27 @@ const ImageModal = (props) => {
setShowFormErrorMsg(false);
setFileReaderResult(false);
setImageAltText('');
- }
+ };
- const onFileDrop = (files) => {
- if (files[0].size > MAX_IMAGE_SIZE) {
- dispatch(addToast(createLocalizedNotificationPayload(NOTIFICATION_TYPES.error, 'imageSizeError')));
- return;
- }
- const file = files[0]; // Only one file is supported for now.
- const fileReader = new FileReader();
- fileReader.addEventListener(
- 'load',
- () => {
- setFileReaderResult(fileReader.result);
- },
- false,
- );
- fileReader.readAsDataURL(file);
- }
-
- const getImagePreview = () => {
- if (fileReaderResult) {
- return ;
+ const onFileChange = async (files) => {
+ try {
+ const file = files[0];
+
+ const compressed = await compressFile(file, MAX_IMAGE_SIZE, 'image/webp');
+ const blob = await fileToDataUri(compressed);
+
+ setFileReaderResult(blob);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+
+ dispatch(addToast(createLocalizedNotificationPayload(NOTIFICATION_TYPES.error, 'imageFileUploadError')));
}
- return false;
- }
+ };
const setImageAltTextFn = (event) => {
setImageAltText(event.target.value);
- }
+ };
const validateForm = () => {
const inputErrors = {
@@ -67,19 +58,16 @@ const ImageModal = (props) => {
};
return isFormValid(inputErrors);
- }
+ };
const confirmImage = () => {
if (validateForm()) {
- props.onSubmit(fileReaderResult, imageAltText);
+ onSubmit(fileReaderResult, imageAltText);
resetState();
} else {
setShowFormErrorMsg(true);
}
- }
-
- const { isOpen, intl, onClose } = props;
- const dropZoneClass = fileReaderResult ? 'dropzone preview' : 'dropzone';
+ };
const titleId = 'image-modal-title';
const descriptionId = 'image-modal-description';
@@ -100,33 +88,34 @@ const ImageModal = (props) => {
-
- {
- ({getRootProps, getInputProps}) => (
- <>
- {getImagePreview()}
-
- >
- )
- }
-
-
-
-
-
-
-
-
-
+ {fileReaderResult && (
+
+
+
+ )}
+ }
+ helperText={}
+ language={intl.locale}
+ onChange={onFileChange}
+ />
+
+ }
className='sectionImageCaptionInput'
value={imageAltText}
onChange={setImageAltTextFn}
@@ -148,13 +137,12 @@ const ImageModal = (props) => {
);
-}
+};
ImageModal.propTypes = {
isOpen: PropTypes.bool,
- intl: PropTypes.object,
onClose: PropTypes.func,
onSubmit: PropTypes.func,
};
-export default injectIntl(ImageModal);
+export default ImageModal;
diff --git a/src/components/admin/Phase.jsx b/src/components/admin/Phase.jsx
index f7e405e52..befcd5581 100644
--- a/src/components/admin/Phase.jsx
+++ b/src/components/admin/Phase.jsx
@@ -1,5 +1,5 @@
/* eslint-disable react/forbid-prop-types */
-import React from 'react';
+import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, useIntl } from 'react-intl';
import { Button, Checkbox, IconTrash, TextInput } from 'hds-react';
@@ -8,12 +8,25 @@ import { useDispatch } from 'react-redux';
import { createNotificationPayload } from '../../utils/notify';
import { addToast } from '../../actions/toast';
-const Phase = (props) => {
- const { phaseInfo, indexNumber, onDelete, onChange, onActive, languages, errors } = props;
-
+const Phase = ({ phaseInfo, indexNumber, onDelete, onChange, onActive, languages, errors }) => {
const dispatch = useDispatch();
const intl = useIntl();
+ const durationsInitial = languages.reduce((acc, current) => {
+ acc[current] = phaseInfo.schedule[current];
+
+ return acc;
+ }, {});
+
+ const descriptionsInitial = languages.reduce((acc, current) => {
+ acc[current] = phaseInfo.description[current];
+
+ return acc;
+ }, {});
+
+ const [phaseDurations, setPhaseDurations] = useState(durationsInitial);
+ const [phaseDescriptions, setPhaseDescriptions] = useState(descriptionsInitial);
+
const handleRadioOnChange = (event) => {
if (event.target.checked) {
onActive(phaseInfo.id || phaseInfo.frontId);
@@ -73,7 +86,12 @@ const Phase = (props) => {
name={`phase-duration-${indexNumber + 1}`}
label={}
maxLength={50}
- value={phaseInfo.schedule[usedLanguage]}
+ value={phaseDurations[usedLanguage]}
+ onChange={(event) => {
+ const { value } = event.target;
+
+ setPhaseDurations({ ...phaseDurations, [usedLanguage]: value });
+ }}
onBlur={(event) =>
onChange(phaseInfo.id || phaseInfo.frontId, 'schedule', usedLanguage, event.target.value)
}
@@ -85,7 +103,12 @@ const Phase = (props) => {
name={`phase-description-${indexNumber + 1}`}
label={}
maxLength={100}
- value={phaseInfo.description[usedLanguage]}
+ value={phaseDescriptions[usedLanguage]}
+ onChange={(event) => {
+ const { value } = event.target;
+
+ setPhaseDescriptions({ ...phaseDescriptions, [usedLanguage]: value });
+ }}
onBlur={(event) =>
onChange(phaseInfo.id || phaseInfo.frontId, 'description', usedLanguage, event.target.value)
}
diff --git a/src/components/admin/SectionForm.jsx b/src/components/admin/SectionForm.jsx
index 81a25f4e1..068ca5b51 100644
--- a/src/components/admin/SectionForm.jsx
+++ b/src/components/admin/SectionForm.jsx
@@ -6,12 +6,13 @@ import PropTypes from 'prop-types';
import { FormattedMessage, useIntl } from 'react-intl';
import { get, isEmpty } from 'lodash';
import { Button, Card, Checkbox, FileInput, LoadingSpinner, Select } from 'hds-react';
-import imageCompression from 'browser-image-compression';
import { QuestionForm } from './QuestionForm';
import MultiLanguageTextField, { TextFieldTypes } from '../forms/MultiLanguageTextField';
import { sectionShape } from '../../types';
import { isSpecialSectionType } from '../../utils/section';
+import compressFile from '../../utils/images/compressFile';
+import fileToDataUri from '../../utils/images/fileToDataUri';
const getFileTitle = (title, language) => {
if (title?.[language] && typeof title[language] !== 'undefined') {
@@ -58,7 +59,7 @@ const fetchFiles = async (data, fileType, language) => {
* MAX_IMAGE_SIZE given in MB
* MAX_FILE_SIZE given in MB
*/
-const MAX_IMAGE_SIZE = 0.9;
+const MAX_IMAGE_SIZE = 1;
const MAX_FILE_SIZE = 70;
const SectionForm = ({
@@ -138,23 +139,6 @@ const SectionForm = ({
}
};
- const fileToDataUri = (file) =>
- new Promise((resolve, reject) => {
- const fileReader = new FileReader();
-
- fileReader.onload = (event) => {
- resolve(event.target.result);
- };
-
- fileReader.onerror = (error) => {
- reject(error);
- };
-
- fileReader.readAsDataURL(file);
- });
-
- const compressFile = async (file, maxSizeMB, fileType) => imageCompression(file, { maxSizeMB, fileType });
-
const onImageChange = async (files) => {
try {
const file = files[0];
@@ -311,7 +295,7 @@ const SectionForm = ({
name='sectionImage'
dragAndDrop
label={}
- accept='.jpeg,.png,.webp,.gif'
+ accept='.jpeg,.jpg,.png,.webp,.gif'
helperText={}
language={language}
onChange={onImageChange}
diff --git a/src/components/forms/FormControlOnChange.jsx b/src/components/forms/FormControlOnChange.jsx
deleted file mode 100644
index 765a782c2..000000000
--- a/src/components/forms/FormControlOnChange.jsx
+++ /dev/null
@@ -1,47 +0,0 @@
-/* eslint-disable camelcase */
-import React from 'react';
-import PropTypes from 'prop-types';
-import FormControl from 'react-bootstrap/lib/FormControl';
-
-class FormControlOnChange extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- value: this.props.defaultValue || '',
- };
- }
-
- UNSAFE_componentWillReceiveProps(newProps) {
- this.setState({
- value: newProps.defaultValue || '',
- });
- }
-
- onChange = (event) => {
- this.setState({
- value: event.target.value,
- });
- };
-
- render() {
- const { type, onBlur, maxLength } = this.props;
- return (
-
- );
- }
-}
-
-FormControlOnChange.propTypes = {
- defaultValue: PropTypes.string,
- type: PropTypes.string,
- onBlur: PropTypes.func,
- maxLength: PropTypes.string,
-};
-
-export default FormControlOnChange;
diff --git a/src/utils/images/__tests__/fileToDataUri.test.js b/src/utils/images/__tests__/fileToDataUri.test.js
new file mode 100644
index 000000000..45e558e25
--- /dev/null
+++ b/src/utils/images/__tests__/fileToDataUri.test.js
@@ -0,0 +1,24 @@
+/* eslint-disable func-names */
+import fileToDataUri from '../fileToDataUri';
+
+describe('fileToDataUri', () => {
+ it('should resolve to a data URI string when file reading is successful', async () => {
+ const mockFile = new Blob(['file content'], { type: 'text/plain' });
+ const expectedDataUri = 'data:text/plain;base64,ZmlsZSBjb250ZW50';
+
+ const result = await fileToDataUri(mockFile);
+
+ expect(result).toBe(expectedDataUri);
+ });
+
+ it('should reject with an error when file reading fails', async () => {
+ const mockFile = new Blob(['file content'], { type: 'text/plain' });
+ const mockError = new Error('File reading failed');
+
+ jest.spyOn(FileReader.prototype, 'readAsDataURL').mockImplementation(function () {
+ this.onerror(mockError);
+ });
+
+ await expect(fileToDataUri(mockFile)).rejects.toThrow('File reading failed');
+ });
+});
diff --git a/src/utils/images/compressFile.js b/src/utils/images/compressFile.js
new file mode 100644
index 000000000..5b9c27e07
--- /dev/null
+++ b/src/utils/images/compressFile.js
@@ -0,0 +1,5 @@
+import imageCompression from 'browser-image-compression';
+
+const compressFile = async (file, maxSizeMB, fileType) => imageCompression(file, { maxSizeMB, fileType });
+
+export default compressFile;
diff --git a/src/utils/images/fileToDataUri.js b/src/utils/images/fileToDataUri.js
new file mode 100644
index 000000000..07c35b637
--- /dev/null
+++ b/src/utils/images/fileToDataUri.js
@@ -0,0 +1,16 @@
+const fileToDataUri = (file) =>
+ new Promise((resolve, reject) => {
+ const fileReader = new FileReader();
+
+ fileReader.onload = (event) => {
+ resolve(event.target.result);
+ };
+
+ fileReader.onerror = (error) => {
+ reject(error);
+ };
+
+ fileReader.readAsDataURL(file);
+ });
+
+export default fileToDataUri;